ES6之前是不能通过代码直接操作二进制数据的,为了方便开发者可以直接操作二进制数据,ES6提出了三个操作二进制数据的接口:ArrayBuffer、TypedArray和DataView
ArrayBuffer
ArrayBuffer代表储存二进制数据的一段内存,但是它不能直接读写,只能通过视图进行读写
比如我们想创建一段32字节的内存数据
const buf = new ArrayBuffer(32)复制代码
创建好了之后我们想要读写这段内存,我们需要通过视图,如:
const buf = new ArrayBuffer(32)const bufView = new Float64Array(buf)console.log(bufView) // Float64Array [ 0, 0, 0, 0 ]复制代码
Float64Array是TypedArray视图的一种,表示64位浮点数(8个字节)的视图
除了使用TypedArray创建视图之外,我们还可以通过DataView
const buf = new ArrayBuffer(32)const bufView = new DataView(buf)console.log(bufView.getUint8(0)) // 0复制代码
关于DataView和TypedArray的区别我们下面会介绍,现在你只需要知道TypedArray不是某个具体的构造函数,而是代表了一组构造函数,而DataView则是一个构造函数
ArrayBuffer.prototype.byteLength
返回实例所分配的内存区域的字节长度
const buf = new ArrayBuffer(32)console.log(buf.byteLength) // 32复制代码
ArrayBuffer.prototype.slice
允许将内存区域的一部分复制成生成一个新的ArrayBuffer对象 ,用法同Array的slice
const buf = new ArrayBuffer(6)const bufView = new Uint16Array(buf)bufView[0] = '刘'.codePointAt(0)bufView[1] = '源'.codePointAt(0)bufView[2] = '泉'.codePointAt(0)const buf2 = buf.slice(0)const bufView2 = new Uint16Array(buf2)for(let i of bufView2) { console.log(String.fromCodePoint(i))}// 刘// 源// 泉复制代码
上面这段代码复制buf对象的所有字节,生成一个新的ArrayBuffer对象buf2,buf2和buf互相不影响,属于两块内存区域
ArrayBuffer.isView
isView是一个静态方法,表示参数是否是ArrayBuffer的视图实例
const buf = new ArrayBuffer(6)const bufView = new Uint16Array(buf)console.log(ArrayBuffer.isView(bufView)) // trueconsole.log(ArrayBuffer.isView(buf)) // false复制代码
TypedArray
TypedArray视图有九种类型,每种类型的数组成员都是同一个数据类型
- Int8Array:8位有符号整数,长度一个字节
- Uint8Array:8位无符号整数,长度一个字节
- Unit8ClampedArray:8位无符号整数,长度一个字节,溢出处理不同
- Int16Array:16位有符号整数,长度为2个字节
- Uint16Array:16位无符号整数,长度为2个字节
- Int32Array:32位有符号整数,长度为4个字节
- Uint32Array:32位无符号整数,长度为4个字节
- Float32Array:32位浮点数,长度为4个字节
- Float64Array:64位浮点数,长度为8个字节
构造函数
TypedArray(buffer, byteOffset = 0, length?)
第一个参数必选:视图对应的底层ArrayBuffer对象 第二个参数可选:视图开始的字节序号,默认是0 第三个参数可选:视图包含的数据个数,默认到本段内存区域结束const buf = new ArrayBuffer(4)const bufView = new Uint8Array(buf, 1, 1)复制代码
注意:byteOffset必须与所建立的数据类型一致,否则会报错
const buf = new ArrayBuffer(4)const bufView = new Uint16Array(buf, 1)// RangeError: start offset of Uint16Array should be a multiple of 2复制代码
TypedArray(length)
视图还可以不通过ArrayBuffer对象,直接分配生成const bufView = new Uint16Array(2)bufView[0] = '徐'.codePointAt(0)bufView[1] = '洁'.codePointAt(0)for(let s of bufView) { console.log(String.fromCodePoint(s))}// 徐// 洁复制代码
TypedArray(typedArray)
可以接受另一个TypedArray实例作为参数,此时生成新的TypedArray实例和传入的TypedArray实例,两者对应的底层内存区域不一样,二者互相不影响const bufView = new Uint16Array(3)bufView[0] = '徐'.codePointAt(0)bufView[1] = '洁'.codePointAt(0)const bufView2 = new Uint16Array(bufView)bufView2[0] = '刘'.codePointAt(0)bufView2[1] = '源'.codePointAt(0)bufView2[2] = '泉'.codePointAt(0)console.log(bufView) // Uint16Array [ 24464, 27905, 0 ]console.log(bufView2) // Uint16Array [ 21016, 28304, 27849 ]复制代码
TypedArray(arrayLikeObject)
也可以接受一个类数组,这时候生成的TypedArray实例会开辟新的内存,而不会在类数组的内存上建立视图const obj = {length: 3}const bufView = new Uint16Array(obj)bufView[0] = '刘'.codePointAt(0)bufView[1] = '源'.codePointAt(0)bufView[2] = '泉'.codePointAt(0)console.log(obj) // { length: 3 }console.log(bufView) // Uint16Array [ 21016, 28304, 27849 ]复制代码
要将一个TypedArray转化为一个普通数组可以调用Array.prototype.slice方法
const normalArray = [].slice.call(typedArray)复制代码
BYTES_PER_ELEMENT
每一种视图的构造函数都有一个BYTES_PER_ELEMENT属性,表示这种数据类型占据的字节数
字符串与ArrayBuffer互相转化
在JavaScript中字符串采用UTF-16编码,即一个字符用两个字节存储,我们可以编写转化函数
字符串转ArrayBuffer
const str2ab = str => { const buf = new ArrayBuffer(str.length * 2) const bufView = new Uint16Array(buf) for(let i = 0, l = str.length;i < l;i++) { bufView[i] = str.codePointAt(i) } return buf}复制代码
ArrayBuffer转字符串
const ab2str = buf => { return String.fromCodePoint.apply(null, new Uint16Array(buf))}复制代码
TypedArray.prototype.buffer
TypedArray的实例的buffer属性返回整段内存区域对应的ArrayBuffer对象,该属性只读
const unit16 = new Uint16Array(1)unit16[0] = '刘'.codePointAt(0)console.log(unit16[0].toString(16)) // 5218const unit8 = new Uint8Array(unit16.buffer)console.log(unit8[0].toString(16)) // 18console.log(unit8[1].toString(16)) // 52复制代码
TypedArray.prototype.byteLength & TypedArray.prototype.byteOffset
byteLength返回TypedArray数组占据的内存长度,单位为字节
byteOffset返回TypedArray数组从底层ArrayBuffer对象的哪个字节开始。两个属性都只读const buf = new ArrayBuffer(8)const v1 = new Uint8Array(buf)const v2 = new Uint16Array(buf, 2, 1)const v3 = new Uint32Array(buf, 4, 1)console.log(v1.byteLength, v1.byteOffset) // 8 0console.log(v2.byteLength, v2.byteOffset) // 2 2console.log(v3.byteLength, v3.byteOffset) // 4 4复制代码
TypedArray.prototype.length
length标书TypedArray数组还有多少成员
const buf = new ArrayBuffer(8)const v1 = new Uint16Array(buf)console.log(v1.length) // 4console.log(v1.byteLength) // 8复制代码
TypedArray.prototype.set
用于复制数组,也就是将一段内存完全复制到另一段内存
const v1 = new Uint8Array(4)v1[0] = 1v1[1] = 2v1[2] = 3v1[3] = 4const v2 = new Uint8Array(4)const v3 = new Uint8Array(6)v2.set(v1)v3.set(v1, 2)console.log(v2) // Uint8Array [ 1, 2, 3, 4 ]console.log(v3) // Uint8Array [ 0, 0, 1, 2, 3, 4 ]复制代码
同时我们还可以对set指定第二个参数,表示从target哪一个成员开始复制,默认是0
TypedArray.prototype.subarray & TypedArray.prototype.slice
subarray和slice用法一模一样,用法同Array.slice。当参数为-1表示倒数第一个位置,-2表示倒数第二个位置,以此类推
const v1 = new Uint8Array(4)v1[0] = 1v1[1] = 2v1[2] = 3v1[3] = 4const v2 = v1.subarray(0, 2)const v3 = v1.subarray(-1)const v4 = v1.slice(1, 3)const v5 = v1.slice(-1)console.log(v2) // Uint8Array [ 1, 2 ]console.log(v3) // Uint8Array [ 4 ]console.log(v4) // Uint8Array [ 2, 3 ]console.log(v5) // Uint8Array [ 4 ]复制代码
TypedArray.of
静态方法,用于将参数转为一个TypedArray实例
const v1 = Uint16Array.of('刘'.codePointAt(0), '源'.codePointAt(0), '泉'.codePointAt(0))console.log(v1) // Uint16Array [ 21016, 28304, 27849 ]复制代码
我们也可以这样初始化一个TypedArray实例
const v1 = new Uint16Array(['刘'.codePointAt(0), '源'.codePointAt(0), '泉'.codePointAt(0)])console.log(v1) // Uint16Array [ 21016, 28304, 27849 ]复制代码
或者
const v1 = new Uint16Array(3)v1[0] = '刘'.codePointAt(0)v1[1] = '源'.codePointAt(0)v1[2] = '泉'.codePointAt(0)console.log(v1) // Uint16Array [ 21016, 28304, 27849 ]复制代码
TypedArray.from
静态方法,接受一个类数组,返回一个基于此结构TypedArray实例,用法可参考Array.from
const v1 = Uint16Array.from({length: 3})console.log(v1) // Uint16Array [ 0, 0, 0 ]复制代码
还可以将一种TypedArray实例转化为另一种
const v1 = Uint16Array.from(Uint8Array.of(1, 2, 3))console.log(v1) // Uint16Array [ 1, 2, 3 ]console.log(v1.byteLength) // 6复制代码
from还可以接收一个函数作为第二个函数,用来对每个元素进行遍历,功能类似map
const int8 = Int8Array.of(127, 126, 125).map(x => x * 2)console.log(int8) // Int8Array [ -2, -4, -6 ] 发生溢出const int16 = Int16Array.from(Int8Array.of(127, 126, 125), x => x * 2)console.log(int16) // Int16Array [ 254, 252, 250 ] 没有溢出复制代码
第一个发生了溢出,这个很显然,因为Int8Array的每个成员表示的范围为-128-127,
而第二个没有发生溢出,说明from会将第一个参数指定的数组先复制到另一块内存中,然后在对结果进行处理,并不是直接对第一个参数指定的数组进行遍历
DataView
用于处理数据成员是多种类型的情况,除此之外还支持字节序
初始化一个DataView对象const buf = new ArrayBuffer(5)const dv = new DataView(buf)复制代码
DataView实例下有buffer,byteLength,byteOffset含义和用法同TypedArray
DataView实例提供了8个方法用于读取内存
- getInt8,读取一个字节,返回一个8位整数
- getUint8,读取一个字节,返回一个无符号的8位整数
- getInt16,读取二个字节,返回一个16位整数
- getUint16,读取二个字节,返回一个无符号的16位整数
- getInt32,读取四个字节,返回一个32位整数
- getUint32,读取四个字节,返回一个无符号的32位整数
- getFloat32,读取四个字节,返回一个32位浮点数
- getFloat64,读取八个字节,返回一个64位浮点数
这一系列的get方法的参数都是一个字节序号,不允许为负,表示从哪个字节开始读取
const buf = new ArrayBuffer(10)const dv = new DataView(buf)console.log(dv.getInt8(0)) // 0console.log(dv.getInt16(1)) // 0console.log(dv.getInt32(2)) // 0复制代码
当一次读取两个以上字节的数据,需要明确数据的存储方式,小端字节序还是大端字节序,默认采用大端字节序存储(false)
const str2ab = str => { const buf = new ArrayBuffer(str.length * 2) const bufView = new Uint16Array(buf) for(let i = 0, l = str.length;i < l;i++) { bufView[i] = str.codePointAt(i) } return buf}const str = '刘源泉'const buf = str2ab(str)for(let i of new Uint16Array(buf)) { console.log(i.toString(16))}console.log('-----------------')const dv = new DataView(buf)console.log(dv.getUint8(0).toString(16)) // 18console.log(dv.getUint8(1).toString(16)) // 52console.log(dv.getUint8(2).toString(16)) // 90console.log(dv.getUint8(3).toString(16)) // 6econsole.log(dv.getUint8(4).toString(16)) // c9console.log(dv.getUint8(5).toString(16)) // 6cconsole.log(dv.getUint16(0).toString(16)) // 1852 大端字节序console.log(dv.getUint16(0, false).toString(16)) // 1852 大端字节序console.log(dv.getUint16(0, true).toString(16)) // 5218 小端字节序复制代码
同样的写入内存也提供了八个方法
- setInt8,写入1个字节的8位整数
- setUint8,写入1个字节的8位无符号整数
- setInt16,写入2个字节的16位整数
- setUint16,写入2个字节的16位无符号整数
- setInt32,写入4个字节的32位整数
- setUint32,写入4字节的32位无符号整数
- setFloat32,写入4个字节的32位浮点数
- setFloat64,写入8个字节的64位浮点数
这一系列的set方法,接受2个参数:第1个参数是字节序号,第2个参数表示写入的数据,当写入两个字节以上的数据时,需要提供第三个参数,表示数据的存储方式,默认是大端字节序(false)
const buf = new ArrayBuffer(32)const dv = new DataView(buf)console.log('刘'.codePointAt(0).toString(16)) // 5218console.log('源'.codePointAt(0).toString(16)) // 6e90console.log('---------')dv.setUint16(0, '刘'.codePointAt(0), true) // 小端字节序写入dv.setUint16(2, '源'.codePointAt(0), false) // 大端字节序写入console.log(dv.getUint16(0, false).toString(16)) // 1852 // 大端字节序读取console.log(dv.getUint16(0, true).toString(16)) // 5218 // 小端字节序读取console.log(dv.getUint16(2, false).toString(16)) // 6e90 // 大端字节序读取console.log(dv.getUint16(2, true).toString(16)) // 906e // 小端字节序读取复制代码
如何判断计算机使用的字节序,可以用下面这个方法
const littleEnidan = (() => { const buf = new ArrayBuffer(2) const dv = new DataView(buf) dv.setInt16(0, 0x0001, true) return new Int16Array(buf)[0] === 0x0001})()console.log(littleEnidan)复制代码
如果返回true,表示小端字节序,否则是大端字节序
二进制数组的应用
API讲的差不多了,该说下二进制数组的应用
XHR2
XHR2允许服务器返回二进制数据,当我们知道服务器会返回二进制数据,我们需要设置responseType为arraybuffer,请求成功之后response就是返回给我们的二进制数据,我们需要创建视图去进行读写操作
const xhr = new XMLHttpRequest()xhr.open('GET', url)xhr.responseType = 'arraybuffer'xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { const arraybuffer = xhr.response // 二进制数组处理 }}xhr.send()复制代码
Canvas
Canvas中操作像素的方法有三个createImageData、getImageData和putImageData
而像素数据是Unit8ClampedArray数组,因为Unit8ClampedArray的溢出处理比其他TypedArray处理起来更方便,确保小于0的值设为0,大于255的值设为255
下面给出两个Canvas操作像素的demo
一个是随着鼠标的移动动态改变文字的颜色
const canvas = document.getElementById('canvas')const h1 = document.getElementById('h1')const ctx = canvas.getContext('2d')const img = new Image()img.onload = function() { ctx.drawImage(img, 0, 0, 300, 150) canvas.addEventListener('mousemove', function(event) { const x = event.layerX const y = event.layerY const imageData = ctx.getImageData(x, y, 1, 1) const unit8 = imageData.data const color = `rgba(${unit8[0]}, ${unit8[1]}, ${unit8[2]}, ${unit8[3]})` h1.style.color = color h1.textContent = color })}img.src = 'url'复制代码
另外一个是图片灰度和颜色反相
const canvas = document.getElementById('canvas')const invertbtn = document.getElementById('invertbtn')const grayscalebtn = document.getElementById('grayscalebtn')const ctx = canvas.getContext('2d')const img = new Image()img.onload = function() { ctx.drawImage(img, 0, 0, 300, 150) const imageData = ctx.getImageData(0, 0, 300, 150) const unit8 = imageData.data invertbtn.addEventListener('click', function() { for(let i = 0;i < unit8.length; i += 4) { unit8[i] = 255 - unit8[i] unit8[i + 1] = 255 - unit8[i + 1] unit8[i + 2] = 255 - unit8[i + 2] } ctx.putImageData(imageData, 0, 0) }) grayscalebtn.addEventListener('click', function() { for(let i = 0;i < unit8.length; i += 4) { const avg = (unit8[i] + unit8[i + 1] + unit8[i + 2]) / 3 unit8[i] = avg unit8[i + 1] = avg unit8[i + 2] = avg } ctx.putImageData(imageData, 0, 0) })}复制代码
Canvas对图片处理就是对二进制像素数据的运算,至于该怎么进行运算大家可以到网上找下相关的运算规则,比如亮度,灰度,透明度等
Fetch
Fetch取回的数据就是ArrayBuffer对象
fetch(url).then(request => request.arrayBuffer()).then(arrayBuffer => {})复制代码
File
当我们上传图片时,我们可以使用FileReader将图片读取成ArrayBuffer,然后可以使用视图对ArrayBuffer进行处理,处理完成之后在上传到服务器或展示在其它Canvas元素中
const input = document.getElementById('input')const read = document.getElementById('read')read.addEventListener('click', function(e) {const reader = new FileReader()reader.addEventListener('load', processimage, false) reader.readAsArrayBuffer(input.files[0])})const processimage = function(e) { const buffer = e.target.result const dv = new DataView(buffer) // 二进制数组处理}复制代码
最后
关于二进制数组的应用其实还有两个:SharedArrayBuffer和WebSocket。后面讲到这两点的时候在补上,大家如果感兴趣的话可以查阅相关资料。
JavaScript学习之路很有很长
你们的打赏是我写作的动力