一张图说明
为了说明他们之间的关系,我画了一张图,其中箭头不代表包含关系,是底层到上层的关系,我们在后面会分别说明一下
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 127 | 1 | 8 位二进制有符号整数 | byte | int8_t |
Uint8Array | 0 to 255 | 1 | 8 位无符号整数(超出范围后从另一边界循环) | octet | uint8_t |
Uint8ClampedArray | 0 to 255 | 1 | 8 位无符号整数(超出范围后为边界值) | octet | uint8_t |
Int16Array | -32768 to 32767 | 2 | 16 位二进制有符号整数 | short | int16_t |
Uint16Array | 0 to 65535 | 2 | 16 位无符号整数 | unsigned short | uint16_t |
Int32Array | -2147483648 to 2147483647 | 4 | 32 位二进制有符号整数 | long | int32_t |
Uint32Array | 0 to 4294967295 | 4 | 32 位无符号整数 | unsigned long | uint32_t |
Float32Array | 1.2 ×10-38 to 3.4 ×1038 | 4 | 32 位 IEEE 浮点数(7 位有效数字,如 1.1234567 ) | unrestricted float | float |
Float64Array | 5.0 ×10-324 to 1.8 ×10308 | 8 | 64 位 IEEE 浮点数(16 有效数字,如 1.123...15 ) | unrestricted double | double |
BigInt64Array | -263 to 263-1 | 8 | 64 位二进制有符号整数 | bigint | int64_t (signed long long) |
BigUint64Array | 0 to 264-1 | 8 | 64 位无符号整数 | bigint | uint64_t (unsigned long long) |
他们分别都是构造函数,通过这些构造函数生成的数组,和普通数组类似,可以使用所有数组的方法,它们和普通数组的区别是:
- TypeArray 元素都是一个类型的
- TypeArray 元素是连续的,不会有空位
- TypeArray 模式元素的初始值都是 0
- 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/…