搞懂 ArrayBuffer、TypedArray、DataView 的对比和使用

4,424 阅读7分钟

一张图说明

为了说明他们之间的关系,我画了一张图,其中箭头不代表包含关系,是底层到上层的关系,我们在后面会分别说明一下

image.png

ArrayBuffer

ArrayBuffer 对象代表存储二进制数据的一段内存,是一个字节数组。

它不能被直接读写,需要创建视图来对它进行操作,视图可以以指定格式操作二进制数据。

ArrayBuffer 也是一个构造函数,可以通过它创建连续的内存区域,参数是内存大小(单位字节),默认初始值都是 0

const buf = new ArrayBuffer(32)

视图

ArrayBuffer 需要通过视图进行操作,视图会有两种形式:类型化数组(TypedArray) 或者 DataView,TypeArray 读写 11 种特定类型的二进制数据,DataView 用来读写自定义复合类型的二进制数据

我们在分别介绍一下

类型化数组(TypedArray)

TypeArray 是 ArrayBuffer 的一个二进制数据的操作视图,实际上,没有名为 TypeArray 的全局变量,也没有名为 TypeArray 的构造函数,我认为可以把它理解为一个 11 种特定类型视图的集合,是一个统称。

这 11 种特定类型视图分别是(内容摘自 MDN):

类型单个元素值的范围大小(bytes)描述Web IDL 类型C 语言中的等价类型
Int8Array-128 to 12718 位二进制有符号整数byteint8_t
Uint8Array0 to 25518 位无符号整数(超出范围后从另一边界循环)octetuint8_t
Uint8ClampedArray0 to 25518 位无符号整数(超出范围后为边界值)octetuint8_t
Int16Array-32768 to 32767216 位二进制有符号整数shortint16_t
Uint16Array0 to 65535216 位无符号整数unsigned shortuint16_t
Int32Array-2147483648 to 2147483647432 位二进制有符号整数longint32_t
Uint32Array0 to 4294967295432 位无符号整数unsigned longuint32_t
Float32Array1.2×10-38 to 3.4×1038432 位 IEEE 浮点数(7 位有效数字,如 1.1234567unrestricted floatfloat
Float64Array5.0×10-324 to 1.8×10308864 位 IEEE 浮点数(16 有效数字,如 1.123...15)unrestricted doubledouble
BigInt64Array-263 to 263-1864 位二进制有符号整数bigintint64_t (signed long long)
BigUint64Array0 to 264-1864 位无符号整数bigintuint64_t (unsigned long long)

他们分别都是构造函数,通过这些构造函数生成的数组,和普通数组类似,可以使用所有数组的方法,它们和普通数组的区别是:

  1. TypeArray 元素都是一个类型的
  2. TypeArray 元素是连续的,不会有空位
  3. TypeArray 模式元素的初始值都是 0
  4. TypeArray 只是一层视图,数据都存储到底层的 ArrayBuffer 中

那么,如何使用这些视图来对 ArrayBuffer 进行操作呢?

const typeArray = new Int8Array(8);
typeArray[0] = 32;
console.log(typeArray);  // [32, 0, 0, 0, 0, 0, 0, 0]

const typeArray1 = new Int8Array(typeArray);
typeArray1[1] = 42;
console.log(typeArray1); // [32, 42, 0, 0, 0, 0, 0, 0]

下面是各种 TypeArray 构造函数的用法举例,注意这里只是语法格式,实际使用时需要替换为各种构造函数。

// 下面代码是语法格式,不能直接运行,
// TypedArray 关键字需要替换为各种构造函数。
new TypedArray(); // ES2017中新增
new TypedArray(length);
new TypedArray(typedArray);
new TypedArray(object);
new TypedArray(buffer [, byteOffset [, length]]);

如果我们在 ArrayBuffer 上新建视图的话

// 生成8个字节内存空间
const buf = new ArrayBuffer(8);
const int8Array = new Int8Array(buf, 0);
int8Array[3] = 32;
const int16Array = new Int16Array(buf, 0, 4);
int16Array[0] = 42;
console.log(int16Array);  // [42, 8192, 0, 0] 因为第三个字节被设置为 32,于是使用两个字节表示一个整数时,就变成了 00010000 00000000,也就是 2 的 13 次方,8192
console.log(int8Array);   // [42, 0, 0, 32, 0, 0, 0, 0]

这是我们可以看到,使用 int8 和 int16 两种方式新建的视图是相互影响的,都是直接修改的底层 buffer 的数据,他们只是操作底层 buffer 数据的两种视图。

另外,普通数组的操作方法,对 TypeArray 也完全适用。和普通数组相比 TypeArray 的最大优势是可以直接操作内存,另外,不需要做数据类型的转换,于是速度要快很多。

DataView

如果一段数据中包括多种类型(比如从服务器传来的 http 数据),除了可以通过分别设置 TypeArray 起始字节和长度外,还可以使用 DataView 来做自定义的复合视图。

在初始设计上 ArrayBuffer 的 TypedArray 视图,是用来向网卡、声卡等本机设备传递数据;DataView 是用来处理网络设备传来的数据的,并且支持设置字节序(将在后面讲到)

DataView 本身也是一个构造函数

new DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);

举个例子:

const buf = new ArrayBuffer(24);
const dataview = new DataView(buf);

这样就新建了一个 DataView 视图,DataView 实例提供了 10 种方法读写内存

具体可以查看 MDN:developer.mozilla.org/en-US/docs/…

DataView.prototype.getInt8() 为例

dataview.getInt8(byteOffset)

表示 DataView 实例从第 byteOffset 字节开始,读取一个有符号的 8bit 整数(一个字节),继续前面的举个例子:

const buf = new ArrayBuffer(24);
const dataview = new DataView(buf);

dataview.setInt8(1, 3);
dataview.getInt8(1);     // 3

这就是利用实例上的不同方法,进行内存读取和写入的操作

到这里你可能认为,如果通过不同类型的 TypeArray,指定起始字节和长度,也能达到一样的效果,嗯现在看是的,但是 DataView 还有另外一个特性:设置字节序,我们进到下一章看一下

字节序

什么是字节序

首先说一下什么是字节序,字节序是数值在内存中的存储方式。分为小端字节序(little-endian)和大端字节序(big-endian)两种

所有的英特尔处理器都使用小端字节序,我们个人电脑基本都是小端字节序,小端字节序会把最不重要的放在最前,可类比欧洲通用的日期书写方式(例如,31 December 2050。年份是最重要的,月份其次,日期最后)

大端字节序则是相反的顺序,可类比 ISO 日期格式(例如 2050-12-31)。big-endian 通常被称作"网络字节顺序"("network byte order"), 因为互联网标准通常要求数据使用 big-endian 存储,从标准 Unix 套接字(socket)层开始,一直到标准化网络的二进制数据结构。

字节序和 TypedArray、DataView 的关系

TypedArray 中,字节序会跟随系统的字节序,于是基本都是小端字节序,是不支持自己设置的,于是就会带来一个问题:如果从网络请求来的数据是大端字节序,会导致数据无法解析。

相比之下,DataView 可以支持设置字节序,举个例子:

const buffer = new ArrayBuffer(24);
const dv = new DataView(buffer);

// 小端字节序
const v1 = dv.getUint16(1, true);

// 大端字节序
const v2 = dv.getUint16(3, false);

// 大端字节序
const v3 = dv.getUint16(3);

DataView 实例方法的第二个参数,可以用来设置字节序,默认是大端字节序

如果不确定计算机上的字节序,可以通过这个方法来判断:

const littleEndian = (function() {
  const buffer = new ArrayBuffer(2);
  new DataView(buffer).setInt16(0, 256, true);
  return new Int16Array(buffer)[0] === 256;
})();

如果返回true,就是小端字节序;如果返回false,就是大端字节序。

这部分具体可以参考:es6.ruanyifeng.com/#docs/array…

参考

es6.ruanyifeng.com/#docs/array…

ArrayBuffer:developer.mozilla.org/zh-CN/docs/…

TypedArray:developer.mozilla.org/zh-CN/docs/…

字节序:developer.mozilla.org/zh-CN/docs/…