二进制数组的操作

本文详细介绍了ES6中操作二进制数据的三种核心接口:ArrayBuffer、TypedArray和DataView,包括它们的基本用法、属性和方法,以及在实际场景如XHR2、Canvas和Fetch中的应用。
摘要由CSDN通过智能技术生成

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学习之路很有很长

你们的打赏是我写作的动力


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值