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)) // true
console.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)) // 5218
const unit8 = new Uint8Array(unit16.buffer)
console.log(unit8[0].toString(16)) // 18
console.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 0
console.log(v2.byteLength, v2.byteOffset) // 2 2
console.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) // 4
console.log(v1.byteLength) // 8
复制代码
TypedArray.prototype.set
用于复制数组,也就是将一段内存完全复制到另一段内存
const v1 = new Uint8Array(4)
v1[0] = 1
v1[1] = 2
v1[2] = 3
v1[3] = 4
const 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] = 1
v1[1] = 2
v1[2] = 3
v1[3] = 4
const 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)) // 0
console.log(dv.getInt16(1)) // 0
console.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)) // 18
console.log(dv.getUint8(1).toString(16)) // 52
console.log(dv.getUint8(2).toString(16)) // 90
console.log(dv.getUint8(3).toString(16)) // 6e
console.log(dv.getUint8(4).toString(16)) // c9
console.log(dv.getUint8(5).toString(16)) // 6c
console.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)) // 5218
console.log('源'.codePointAt(0).toString(16)) // 6e90
console.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学习之路很有很长
你们的打赏是我写作的动力