【译】JavaScript 类型数组(JavaScript typed arrays)

JavaScript 类型数组(JavaScript typed arrays)。

写在前面

原文来自 MDN JavaScript 主题的高阶教程部分,一共 5 篇,分别涉及继承和原型链、严格模式、类型数组、内存管理、并发模型和事件循环。本篇是第 3 篇,关于类型数组

2023-06-23 更新: 今天发现,MDN 原文结构进行了调整,原来高级教程的 5 篇文章只剩下了 3 篇,分别是继承和原型链、内存管理、并发模型和事件循环,其中,严格模式被转移到了 References/Misc 下,类型数组被转移到了 JavaScript Guide 下。

JavaScript 类型数组是一种类数组对象,它们提供了一种在内存缓冲区中读写二进制数据的机制。你也许已经知道,Array大小能够动态伸缩而且可以存储 JavaScript 的任何类型。JavaScript 引擎进行了一些优化,使得操作这些数组很快。

然而,随着 web 应用变得越来越强大,如果需要一些额外功能,例如音视频数据操作、使用 WebSockets 访问原始数据等等,很明显,如果 JavaScript 能够快速轻松地操作原始二进制数据会很有帮助。这就是类型数据提出的原因。JavaScript 类型数组的每一项都是某种格式的二进制数据,这些数据格式范围从 8 位整数到 64 位浮点数。

然而,不要吧类型数组与普通数组混淆,因为使用Array.isArray()检测类型数组会返回false。除此之外,并不是所有普通数组上的方法都存在于类型数组上(例如 push 和 pop)。

为了最大化可扩展性和提升效率,JavaScript 类型数组被分成两个部分:缓冲区(buffers)和视图(views)。缓冲区(由ArrayBuffer对象实现)是一个包含着一大块数据的对象,它没有任何格式,也没提供机制来访问其内容。为了访问缓冲区中的内存,你需要使用视图。视图提供了一个上下文,其中包含了一种数据类型、起始偏移量以及元素数量。视图将数据转化为一个类型数组。

ArrayBuffer

ArrayBuffer是一种数据类型,其被用于表示一块通用且固定大小的二进制数据缓冲区。你无法直接操作ArrayBuffer的内容,而是需要创建一个类型数组视图或者是一个DataView,它们以一种特殊格式表示着这块缓冲区,同时可以使用它们来读写缓冲区中的内容。

类型数组的视图具有一个描述性的名字,它们为所有通用的数值类型(例如Int8Uint32Float64等等)提供了一种视图。还有一种特殊的视图叫做Uint8ClampedArray,它将取值控制在 0-255 之间,这有助于Canvas 的数据处理

类型取值范围大小(单位:字节)说明Web IDL 类型对应的 C 语言类型
Int8Array-128 到 12718 位有符号整数byteint8_t
Uint8Array0 到 25518 位无符号整数octetuint8_t
Uint8ClampedArray0 到 25518 位无符号整数(取值受控)octetuint8_t
Int16Array-32768 到 32767216 位有符号整数shortint16_t
Uint16Array0 到 65535216 位无符号整数unsigned shortuint16_t
Int32Array-2147483648 到 2147483647432 位有符号整数longint32_t
Uint32Array0 到 4294967295432 位无符号整数unsigned longuint32_t
Float32Array1.2x10-38 到 3.4x1038432 位单精度 IEEE 标准浮点数(7 位有效数字例如: 1.123456unrestricted floatfloat
Float64Array5.0x10-324 到 1.8x10308832 位双精度 IEEE 标准浮点数(16 位有效数字例如: 1.123...15unrestricted doubledouble
BigInt64Array-263 到 263-1864 位有符号整数bigintint64_t (signed long long)
BigUint64Array0 到 264-1854 位无符号整数bigintuint64_t (unsigned long long)

DataView提供了底层 getter/setter API,能够读写缓冲区数据。当处理不同类型数据时很有用。类型数组视图的操作遵循系统原生的字节序(Endianness)进行存储。使用DataView则可以控制字节序,默认是大端,也可以使用 getter/setter 方法时设置成小端。

下面是一些使用到类型数组的 Web API,除此之外还有一些其他的,而且一直有更多 API 被加入进来:

FileReader.prototype.readAsArrayBuffer()

FileReader.prototype.readAsArrayBuffer()方法可以读取指定的BlobFile内容。

XMLHttpRequest.prototype.send()

XMLHttpRequest对象实例的send()方法现在支持类型数组,以ArrayBuffer对象作为参数。

ImageData.data

ImageData.data是一个Uint8ClampedArray格式的一维数组,包含了 RGBA 顺序的整型数据,大小从 0255(包含 0255

首先,我们需要创建一个 buffer,下面的 buffer 为固定的 16 字节长:

1
let buffer = new ArrayBuffer(16);

现在,我们有一块初始值为 0 的内存。但是我们还不能使用它,不过我们可以确认其确实是 16 字节:

1
2
3
4
5
if (buffer.byteLength === 16) {
  console.log("Yes, it's 16 bytes.");
} else {
  console.log("Oh no, it's the wrong size!");
}

为了真正使用它,我们需要创建一个 view。让我们创建一个 view,这个 view 会将 buffer 中的数据看做是 32 位符号整数组成的数组:

1
let int32View = new Int32Array(buffer);

现在我们可以访问数组字段了,就像一个普通数组一样:

1
2
3
for (let i = 0; i < int32View.length; i++) {
  int32View[i] = i * 2;
}

上述代码填充了数组中的 4 个条目(每个条目 4 个字节,一共 16 个字节),值依次为0, 2, 4, 6

当你在同一份数据上创建多份不同的 view 时会非常有趣。例如,接着上面的代码:

1
2
3
4
5
let int16View = new Int16Array(buffer);

for (let i = 0; i < int16View.length; i++) {
  console.log("Entry " + i + ": " + int16View[i]);
}

我们接着创建了一个 16 位符号整数视图,它与先前的 32 位视图共享同一个 buffer,当我们以 16 位视图输出这个 buffer 中的值,结果为0, 0, 2, 0, 4, 0, 6, 0

译者注:当前大部分计算机都是使用小端存储,因此最开始我们通过 32 位视图设置完成后,数据存储为:0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 ...。当使用 16 位视图访问时,相当于(0x00 0x00) (0x00 0x00) (0x02 0x00)...,因此结果为0, 0, 2, 0, ...而不是0, 0, 0, 2, ...

还可以更进一步,例如:

1
2
int16View[0] = 32;
console.log("Entry 0 in the 32-bit array is now " + int32View[0]);

打印结果为"Entry 0 in the 32-bit array is now 32"

换句话说,两个视图数组确实是访问同一个 buffer,将其视为不同格式。你还可以使用其他任意类型数组

通过对同一个 buffer 的数据使用不同视图,每个视图从 buffer 的不同偏移量开始,你可以创建并使用包含不同数据类型的数据结构对象。举个例子,你可以使用复杂的数据结构比如WebGL、data files 或者当使用js-ctypes需要使用的 C 语言数据结构。

当有如下一个 C 语言数据结构:

1
2
3
4
5
struct someStruct {
  unsigned long id;
  char username[16];
  float amountDue;
};

你可以像下面这样访问含有上述格式的 buffer 数据:

1
2
3
4
5
6
7
let buffer = new ArrayBuffer(24);

// ... 将数据读进 buffer ...

let idView = new Uint32Array(buffer, 0, 1);
let usernameView = new Uint8Array(buffer, 4, 16);
let amountDueView = new Float32Array(buffer, 20, 1);

如果想知道amountDue的值,则可以通过amountDueView[0]获取。

注意:C 语言中的数据结构对齐依赖于平台实现。对于结构的填充差异,请仔细考虑并采取一些预防措施。

创建类型数组后,有时候为了享受普通数组的一些便利方法,可以将其转换回普通数组。通过Array.from()可以将其转换回普通数组,如果不支持Array.from(),也可以使用下面代码:

1
2
3
4
let typedArray = new Uint8Array([1, 2, 3, 4]),
  normalArray = Array.prototype.slice.call(typedArray);
normalArray.length === 4;
normalArray.constructor === Array;

ECMAScript (ECMA-262) The definition of ‘TypedArray Objects’ in that specification.

相关内容