JavaScript for impatient programmers (ES2022 edition)
请支持本书:购买捐赠
(广告,请勿屏蔽。)

32 类型化数组:处理二进制数据(高级)



32.1 API 基础

Web 上的许多数据都是文本:JSON 文件、HTML 文件、CSS 文件、JavaScript 代码等。JavaScript 通过其内置字符串可以很好地处理此类数据。

但是,在 2011 年之前,它不能很好地处理二进制数据。类型化数组规范 1.0 于 2011 年 2 月 8 日推出,提供了用于处理二进制数据的工具。在 ECMAScript 6 中,类型化数组被添加到核心语言中,并获得了以前仅适用于普通数组的方法(.map().filter() 等)。

32.1.1 类型化数组的用例

类型化数组的主要用例是

32.1.2 核心类:ArrayBuffer、类型化数组、DataView

类型化数组 API 将二进制数据存储在 ArrayBuffer 的实例中

const buf = new ArrayBuffer(4); // length in bytes
  // buf is initialized with zeros

ArrayBuffer 本身是一个黑盒子:如果您想访问它的数据,必须将其包装在另一个对象中——一个*视图对象*。有两种视图对象可用

20 显示了 API 的类图。

Figure 20: The classes of the Typed Array API.

32.1.3 使用类型化数组

类型化数组的使用方式与普通数组非常相似,但有一些显著区别

32.1.3.1 创建类型化数组

以下代码显示了创建相同类型化数组的三种不同方法

// Argument: Typed Array or Array-like object
const ta1 = new Uint8Array([0, 1, 2]);

const ta2 = Uint8Array.of(0, 1, 2);

const ta3 = new Uint8Array(3); // length of Typed Array
ta3[0] = 0;
ta3[1] = 1;
ta3[2] = 2;

assert.deepEqual(ta1, ta2);
assert.deepEqual(ta1, ta3);
32.1.3.2 包装的 ArrayBuffer
const typedArray = new Int16Array(2); // 2 elements
assert.equal(typedArray.length, 2);

assert.deepEqual(
  typedArray.buffer, new ArrayBuffer(4)); // 4 bytes
32.1.3.3 获取和设置元素
const typedArray = new Int16Array(2);

assert.equal(typedArray[1], 0); // initialized with 0
typedArray[1] = 72;
assert.equal(typedArray[1], 72);

32.1.4 使用 DataView

DataView 的使用方法如下

const dataView = new DataView(new ArrayBuffer(4));
assert.equal(dataView.getInt16(0), 0);
assert.equal(dataView.getUint8(0), 0);
dataView.setUint8(0, 5);

32.2 元素类型

表 20:类型化数组 API 支持的元素类型。
元素 类型化数组 字节 描述
Int8 Int8Array 1 8 位有符号整数 ES6
Uint8 Uint8Array 1 8 位无符号整数 ES6
Uint8C Uint8ClampedArray 1 8 位无符号整数 ES6
(钳制转换) ES6
Int16 Int16Array 2 16 位有符号整数 ES6
Uint16 Uint16Array 2 16 位无符号整数 ES6
Int32 Int32Array 4 32 位有符号整数 ES6
Uint32 Uint32Array 4 32 位无符号整数 ES6
BigInt64 BigInt64Array 8 64 位有符号整数 ES2020
BigUint64 BigUint64Array 8 64 位无符号整数 ES2020
Float32 Float32Array 4 32 位浮点数 ES6
Float64 Float64Array 8 64 位浮点数 ES6

20 列出了可用的元素类型。这些类型(例如,Int32)出现在两个位置

元素类型 Uint8C 很特殊:它不受 DataView 支持,仅用于启用 Uint8ClampedArray。此类型化数组由 canvas 元素使用(它替换了 CanvasPixelArray),否则应避免使用。Uint8CUint8 之间的唯一区别在于如何处理溢出和下溢(如下一小节中所述)。

类型化数组和数组缓冲区使用数字和大整数来导入和导出值

32.2.1 处理溢出和下溢

通常,当值超出元素类型的范围时,将使用模运算将其转换为范围内的值。对于有符号和无符号整数,这意味着

以下函数有助于说明转换是如何工作的

function setAndGet(typedArray, value) {
  typedArray[0] = value;
  return typedArray[0];
}

无符号 8 位整数的模转换

const uint8 = new Uint8Array(1);

// Highest value of range
assert.equal(setAndGet(uint8, 255), 255);
// Overflow
assert.equal(setAndGet(uint8, 256), 0);

// Lowest value of range
assert.equal(setAndGet(uint8, 0), 0);
// Underflow
assert.equal(setAndGet(uint8, -1), 255);

有符号 8 位整数的模转换

const int8 = new Int8Array(1);

// Highest value of range
assert.equal(setAndGet(int8, 127), 127);
// Overflow
assert.equal(setAndGet(int8, 128), -128);

// Lowest value of range
assert.equal(setAndGet(int8, -128), -128);
// Underflow
assert.equal(setAndGet(int8, -129), 127);

钳制转换不同

const uint8c = new Uint8ClampedArray(1);

// Highest value of range
assert.equal(setAndGet(uint8c, 255), 255);
// Overflow
assert.equal(setAndGet(uint8c, 256), 255);

// Lowest value of range
assert.equal(setAndGet(uint8c, 0), 0);
// Underflow
assert.equal(setAndGet(uint8c, -1), 0);

32.2.2 字节序

每当一个类型(例如 Uint16)存储为多个字节的序列时,*字节序*就很重要

字节序往往在每个 CPU 架构上都是固定的,并且在原生 API 中是一致的。类型化数组用于与这些 API 通信,这就是为什么它们的字节序遵循平台的字节序并且不能更改的原因。

另一方面,协议和二进制文件的字节序各不相同,但每个格式在不同平台上都是固定的。因此,我们必须能够以任何一种字节序访问数据。DataView 服务于此用例,并允许您在获取或设置值时指定字节序。

引用维基百科关于字节序的内容:

其他排序也是可能的。这些通常称为*中端序*或*混合端序*。

32.3 有关类型化数组的更多信息

在本节中,«ElementType»Array 代表 Int8ArrayUint8Array 等。ElementTypeInt8Uint8 等。

32.3.1 静态方法 «ElementType»Array.from()

此方法具有类型签名

.from<S>(
  source: Iterable<S>|ArrayLike<S>,
  mapfn?: S => ElementType, thisArg?: any)
  : «ElementType»Array

.from()source 转换为 this 的实例(一个类型化数组)。

例如,普通数组是可迭代的,可以使用此方法进行转换

assert.deepEqual(
  Uint16Array.from([0, 1, 2]),
  Uint16Array.of(0, 1, 2));

类型化数组也是可迭代的

assert.deepEqual(
  Uint16Array.from(Uint8Array.of(0, 1, 2)),
  Uint16Array.of(0, 1, 2));

source 也可以是*类数组对象*

assert.deepEqual(
  Uint16Array.from({0:0, 1:1, 2:2, length: 3}),
  Uint16Array.of(0, 1, 2));

可选的 mapfn 允许您在 source 的元素成为结果的元素之前对其进行转换。为什么要一次性执行*映射*和*转换*这两个步骤?与通过 .map() 单独映射相比,有两个优点

  1. 不需要中间数组或类型化数组。
  2. 在不同精度类型化数组之间转换时,出错的可能性较小。

继续阅读以了解第二个优点的说明。

32.3.1.1 陷阱:在类型化数组类型之间转换时进行映射

静态方法 .from() 可以选择同时进行映射和在类型化数组类型之间转换。如果您使用该方法,出错的可能性较小。

为了了解原因,让我们首先将类型化数组转换为精度更高的类型化数组。如果我们使用 .from() 进行映射,结果会自动正确。否则,您必须先转换再映射。

const typedArray = Int8Array.of(127, 126, 125);
assert.deepEqual(
  Int16Array.from(typedArray, x => x * 2),
  Int16Array.of(254, 252, 250));

assert.deepEqual(
  Int16Array.from(typedArray).map(x => x * 2),
  Int16Array.of(254, 252, 250)); // OK
assert.deepEqual(
  Int16Array.from(typedArray.map(x => x * 2)),
  Int16Array.of(-2, -4, -6)); // wrong

如果我们从类型化数组转换为精度较低的类型化数组,则通过 .from() 进行映射会产生正确的结果。否则,我们必须先映射再转换。

assert.deepEqual(
  Int8Array.from(Int16Array.of(254, 252, 250), x => x / 2),
  Int8Array.of(127, 126, 125));

assert.deepEqual(
  Int8Array.from(Int16Array.of(254, 252, 250).map(x => x / 2)),
  Int8Array.of(127, 126, 125)); // OK
assert.deepEqual(
  Int8Array.from(Int16Array.of(254, 252, 250)).map(x => x / 2),
  Int8Array.of(-1, -2, -3)); // wrong

问题是,如果我们通过 .map() 进行映射,则输入类型和输出类型相同。相反,.from() 从任意输入类型到您通过其接收器指定的输出类型。

32.3.2 类型化数组是可迭代的

类型化数组是可迭代的。这意味着您可以使用 for-of 循环和其他基于迭代的机制

const ui8 = Uint8Array.of(0, 1, 2);
for (const byte of ui8) {
  console.log(byte);
}
// Output:
// 0
// 1
// 2

ArrayBuffer 和 DataView 不可迭代。

32.3.3 类型化数组与普通数组

类型化数组与普通数组非常相似:它们具有 .length,可以通过方括号运算符 [] 访问元素,并且它们具有大多数标准数组方法。它们与普通数组的不同之处在于以下几点

32.3.4 将类型化数组与普通数组相互转换

要将普通数组转换为类型化数组,可以将其传递给类型化数组构造函数(它接受类数组对象和类型化数组)或 «ElementType»Array.from()(它接受可迭代对象和类数组对象)。例如:

const ta1 = new Uint8Array([0, 1, 2]);
const ta2 = Uint8Array.from([0, 1, 2]);
assert.deepEqual(ta1, ta2);

要将类型化数组转换为普通数组,可以使用 Array.from() 或展开运算符(因为类型化数组是可迭代的)。

assert.deepEqual(
  [...Uint8Array.of(0, 1, 2)], [0, 1, 2]
);
assert.deepEqual(
  Array.from(Uint8Array.of(0, 1, 2)), [0, 1, 2]
);

32.3.5 连接类型化数组

与普通数组不同,类型化数组没有 .concat() 方法。解决方法是使用其重载方法 .set()

.set(typedArray: TypedArray, offset=0): void
.set(arrayLike: ArrayLike<number>, offset=0): void

它将现有的 typedArrayarrayLike 复制到接收器中,索引为 offsetTypedArray 是所有具体类型化数组类的虚拟抽象超类。

以下函数使用该方法将零个或多个类型化数组(或类数组对象)复制到 resultConstructor 的实例中。

function concatenate(resultConstructor, ...arrays) {
  let totalLength = 0;
  for (const arr of arrays) {
    totalLength += arr.length;
  }
  const result = new resultConstructor(totalLength);
  let offset = 0;
  for (const arr of arrays) {
    result.set(arr, offset);
    offset += arr.length;
  }
  return result;
}
assert.deepEqual(
  concatenate(Uint8Array, Uint8Array.of(1, 2), [3, 4]),
  Uint8Array.of(1, 2, 3, 4));

32.4 快速参考:索引与偏移量

在准备 ArrayBuffer、类型化数组和 DataView 的快速参考之前,我们需要了解索引和偏移量之间的区别。

参数是索引还是偏移量只能通过查看文档来确定;没有简单的规则。

32.5 快速参考:ArrayBuffer

ArrayBuffer 存储二进制数据,这些数据旨在通过类型化数组和 DataView 进行访问。

32.5.1 new ArrayBuffer()

构造函数的类型签名为:

new ArrayBuffer(length: number)

通过 new 调用此构造函数会创建一个容量为 length 字节的实例。这些字节最初都为 0。

无法更改 ArrayBuffer 的长度;只能创建长度不同的新 ArrayBuffer。

32.5.2 ArrayBuffer 的静态方法

32.5.3 ArrayBuffer.prototype 的属性

32.6 快速参考:类型化数组

各种类型化数组对象的属性分两步介绍:

  1. TypedArray:首先,我们看一下所有类型化数组类的抽象超类(如本章开头的类图 所示)。我将该超类称为 TypedArray,但它不能从 JavaScript 直接访问。TypedArray.prototype 包含类型化数组的所有方法。
  2. «ElementType»Array:具体的类型化数组类称为 Uint8ArrayInt16ArrayFloat32Array 等。这些是通过 new.of.from() 使用的类。

32.6.1 TypedArray<T> 的静态方法

两个静态 TypedArray 方法都由其子类(Uint8Array 等)继承。TypedArray 是抽象的。因此,始终通过子类使用这些方法,这些子类是具体的并且可以具有直接实例。

32.6.2 TypedArray<T>.prototype 的属性

类型化数组方法接受的索引可以为负数(它们的工作方式类似于传统的数组方法)。偏移量必须是非负数。有关详细信息,请参阅 §32.4 “快速参考:索引与偏移量”

32.6.2.1 类型化数组特有的属性

以下属性是类型化数组特有的;普通数组没有这些属性。

32.6.2.2 数组方法

以下方法与普通数组的方法基本相同:

有关这些方法如何工作的详细信息,请参阅 §31.13.3 “Array.prototype 的方法”

32.6.3 new «ElementType»Array()

每个类型化数组构造函数的名称都遵循模式 «ElementType»Array,其中 «ElementType» 是开头表格中的元素类型之一。这意味着类型化数组有 11 个构造函数:

每个构造函数都有四个重载版本,它的行为取决于它接收的参数数量及其类型:

32.6.4 «ElementType»Array 的静态属性

32.6.5 «ElementType»Array.prototype 的属性

32.7 快速参考:DataView

32.7.1 new DataView()

32.7.2 DataView.prototype 的属性

在本节的其余部分中,«ElementType» 指的是以下任意一项:

以下是 DataView.prototype 的属性: