阅读 339

前端开发中的二进制数据

在计算机中,二进制数据是最基础的数据。但是在前端开发中,我们更多时候接触的是高级数据类型 number,需要直接使用二进制数据的场景比较少,下面是几个典型的在前端需要二进制数据的场景:

  • canvas 图像数据处理

  • WebGL 中与显卡通信

  • 字符串编码 TextEncoder

  • Ajax responseType arraybuffer

  • WebAssembly 中与其他编程语言通信

JS 中提供了 ArrayBuffer 来控制二进制数据。ArrayBuffer 表示的是一段固定长度的二进制数据缓冲区,new ArrayBuffer(8) 即可创建一个长度为 8 个字节的二进制数组。但是我们不能直接操作这块数据,ArrayBuffer 是一块二进制数据的缓冲区,它只是简单地存储数据本身,真正如何控制取决于我们使用怎样的方式来读取它,JS 提供了 TypedArray 和 DataView 两种方式读写 ArrayBuffer。

TypedArray

TypedArray 是 JS 内置的一系列类型,目前有 11 种:Int8Array、Uint8Array、Uint8ClampedArray、Int16Array、Uint16Array、Int32Array、Uint32Array、Float32Array、Float64Array、BigInt64Array、BigUint64Array。TypedArray 顾名思义它的特点就是与类型相关,从每种类型的名字上可以很容易地看出他们对应的数据类型,使用对应的 TypedArray 类就可以实现对 ArrayBuffer 读取或写入对应类型的数据。

TypeValue RangeSize in bytesDescriptionWeb IDL typeEquivalent C type
Int8Array-128 to 12718-bit two's complement signed integerbyteint8_t
Uint8Array0 to 25518-bit unsigned integeroctetuint8_t
Uint8ClampedArray0 to 25518-bit unsigned integer (clamped)octetuint8_t
Int16Array-32768 to 32767216-bit two's complement signed integershortint16_t
Uint16Array0 to 65535216-bit unsigned integerunsigned shortuint16_t
Int32Array-2147483648 to 2147483647432-bit two's complement signed integerlongint32_t
Uint32Array0 to 4294967295432-bit unsigned integerunsigned longuint32_t
Float32Array-3.4E38 to 3.4E38 and 1.2E-38 is the min positive number432-bit IEEE floating point number (7 significant digits e.g., 1.234567)unrestricted floatfloat
Float64Array-1.8E308 to 1.8E308 and 5E-324 is the min positive number864-bit IEEE floating point number (16 significant digits e.g., 1.23456789012345)unrestricted doubledouble
BigInt64Array-2^63 to 2^63 - 1864-bit two's complement signed integerbigintint64_t (signed long long)
BigUint64Array0 to 2^64 - 1864-bit unsigned integerbigintuint64_t (unsigned long long)

构造函数:

new TypedArray(); // ES2017中新增,创建一个空的 TypedArray new TypedArray(length); // 创建一个长度为 langth 的 TypedArray new TypedArray(typedArray); // 从一个 typedArray 复制一个新的 typedArray new TypedArray(object); // 调用 TypedArray.from 创建 typedArray,效果同 Array.from new TypedArray(buffer [, byteOffset [, length]]); // 使用指定 buffer 或 buffe 的一部分创建 typedArray,buffer 长度要能够被单个元素占的字节数整除 复制代码

来看一个例子:

const buffer = new ArrayBuffer(16); const typedArray = new Uint8Array(buffer); console.log(typedArray); // output: Uint8Array(16) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] typedArray[0] = 10; typedArray[1] = 16; console.log(typedArray[0]); // output: 10 console.log(typedArray); // output: Uint8Array(16) [10, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] 复制代码

入上表:Uint8Array 每个元素占一个字节,因此我们从长度为 16 字节的 ArrayBuffer 中可以创建一个长度为 16 的 Uint8Array,我们可以像数组一样通过下标取值,可以为其设置值,可以使用大部分 Array 上的方法。 TypedArray 与普通 Array 的区别在于它是固定长度且内存空间连续的,因此能够改变数组长度的 push、pop、splice 方法不能在 TypedArray 上使用。

表中 Uint8Array 能存储的数据范围为 0-255,我们试着存储一个更大的数据进来:

const buffer = new ArrayBuffer(2); const uint8Array = new Uint8Array(buffer); uint8Array[0] = 300; console.log(uint8Array); // output: Uint8Array(2) [44, 0] 复制代码

我们写入了 300,但是读到的是 44,这显然是不符合预期的,因此为了避免内存超出限制,我们需要选择合适的数据格式,这里换成 Uint16Array 即可解决。

const buffer = new ArrayBuffer(2); const uint16Array = new Uint16Array(buffer); uint16Array[0] = 300; console.log(uint16Array); // output: Uint16Array [300] 复制代码

由于 Uint16Array 占用两个字节,这里长度为 2 的 ArrayBuffer 创建的 uint16Array 只有一个值。下图可以理解通过 TypedArray 控制 ArrayBuffer 的原理:

ArrayBuffer

从图中可以看出,实际上 ArrayBuffer 表示的内存空间是一样的,TypedArray 提供的是一个读取内存的单位,我们使用 Uint8Array 时下标 0 控制的是内存中第一个字节,换成 Uint16Array 的下标 0 控制的是前两个字节,以此类推。

那么根据这个原理,我们通过 Uint8Array,写入两个字节是不是可以通过 Uint16Array 读取呢?

const buffer = new ArrayBuffer(2); const uint8Array = new Uint8Array(buffer); uint8Array[0] = 10; uint8Array[1] = 16; const uint16Array = new Uint16Array(buffer); 复制代码

考虑一下这里 uint16Array 是什么?

由于我们在第一个字节中写入了 10,第二个字节写入 16,一个字节占 8 位,看起来应该是 (10 << 8) + 16 = 2576,我们试着打印一下:

console.log(uint16Array); // output: Uint16Array [4106] 复制代码

好像和预期的不一样,我们看到结果并不是 2576 而是 4106,是哪里出了问题呢?

这里涉及到一个字节存储顺序的问题,计算机在存储多字节数据时有两种方式,低位存储在低地址称为小端,高位存储在低地址称为大端。

使用 TypedArray 设置的值以小端方式进行存储的,因此 Uint16Array 在读取时低地址上的数据作为低位,高地址上的数据作为高位,即 10 为低位,16 为高位,因此上面的值为 (16 << 8) + 10 = 4106。

我们回过来看上面的 300 为什么会变成 44。300 转为二进制为 100101100,超出了一个字节,因此按照小堆的方式应该把低位放入第一个字节即 00101100,高位 1 应该放入第二个字节,由于 Uint8Array 只有一个字节,因此高位的 1 丢失,只剩下 00101100 即 44。

我们可以验证一下:

const buffer = new ArrayBuffer(2); const uint16Array = new Uint16Array(buffer); uint16Array[0] = 300; const uint8Array = new Uint8Array(buffer); console.log(uint8Array); // output: Uint8Array(2) [44, 1] 复制代码

Uint8ClampedArray 比较特殊,它在数据超限时不会进行舍弃,而是直接存入 0 或 255,rgb 数据值在 0-255 之间,因此常用于 imageData。

使用 TypedArray 在 buffer 上写入的是同一种类型的数据,但是我们也看到了,实际上写入的都是无差别的二进制信息,实际上数据是什么取决于我们以何种方式读取内存。因此我们对同一块 bffer,我们可以写入和读取多种不同格式的数据。但是由于大小端问题我们写入不同类型数据时会比较麻烦,因此对于不同类型的数据,我们可以使用 DataView 来操作。

DataView

使用 DataView 读写 ArrayBuffer 很简单,我们只需要关注每次写入的字节大小和偏移量即可,DataView 默认以大端方式存储,我们可以在写入值时设置最后一个参数为 true 来指定使用小端,注意写入时如果指定小端读取时也要指定小端读取。

const buffer = new ArrayBuffer(20); const dataView = new DataView(buffer); dataView.setInt8(0, 10); dataView.setInt16(1, 300); dataView.setInt16(3, 400, true); // 使用小端 dataView.setInt8(5, 20); console.log(dataView.getInt8(0), dataView.getInt16(1), dataView.getInt16(3, true /* 使用小端 */), dataView.getInt8(5)); // output: 10 300 400 20 复制代码

为什么 TypedArray 使用小端而 DataView 默认用大端?

因为目前大多计算机 CPU 和内存中使用的都是小端存储的方式,TypedArray 提供的是一组直接操作二进制数据的接口,直接使用小端可以带来更好的性能。而 DataView 可以组装多种类型数据,这种场景多用于数据序列化反序列化和传输,而在传输中,对于流式数据,我们只需要在数据到来时按照内存顺序存储即可还原大端序列,这时大端明显更高效,在 TCP/IP 中使用的也是大端。当然 DataView 可以指定使用小端,因此实际使用时 TypedArray 做的事情也可以使用 DataView 来完成。


作者:丨隋堤倦客丨
链接:https://juejin.cn/post/7025818772531314724


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐