20. 类型化数组
目录
请支持本书:购买 (PDF, EPUB, MOBI)捐赠
(广告,请不要屏蔽。)

20. 类型化数组



20.1 概述

类型化数组是 ECMAScript 6 中用于处理二进制数据的 API。

代码示例

const typedArray = new Uint8Array([0,1,2]);
console.log(typedArray.length); // 3
typedArray[0] = 5;
const normalArray = [...typedArray]; // [5,1,2]

// The elements are stored in typedArray.buffer.
// Get a different view on the same data:
const dataView = new DataView(typedArray.buffer);
console.log(dataView.getUint8(0)); // 5

ArrayBuffer 的实例存储要处理的二进制数据。两种 视图 用于访问数据

以下浏览器 API 支持类型化数组(详细信息在专门的章节中提到

20.2 介绍

我们在网络上遇到的很多数据都是文本:JSON 文件、HTML 文件、CSS 文件、JavaScript 代码等等。对于处理此类数据,JavaScript 内置的字符串数据类型非常有效。然而,直到几年前,JavaScript 还很不擅长处理二进制数据。2011 年 2 月 8 日,类型化数组规范 1.0 标准化了处理二进制数据的工具。到目前为止,类型化数组已经得到 各种引擎的良好支持。随着 ECMAScript 6 的出现,它们成为了核心语言的一部分,并在此过程中获得了许多以前只能用于数组的方法(map()filter() 等)。

类型化数组的主要用例是

类型化数组 API 中有两种对象协同工作

这是类型化数组 API 结构的示意图(值得注意的是:所有类型化数组都有一个共同的超类)

20.2.1 元素类型

API 支持以下元素类型

元素类型 字节数 描述 C 类型
Int8 1 8 位有符号整数 signed char
Uint8 1 8 位无符号整数 unsigned char
Uint8C 1 8 位无符号整数(钳制转换) unsigned char
Int16 2 16 位有符号整数 short
Uint16 2 16 位无符号整数 unsigned short
Int32 4 32 位有符号整数 int
Uint32 4 32 位无符号整数 unsigned int
Float32 4 32 位浮点数 float
Float64 8 64 位浮点数 double

元素类型 Uint8C 很特殊:DataView 不支持它,它只存在于启用 Uint8ClampedArraycanvas 元素使用此类型化数组(它取代了 CanvasPixelArray)。Uint8CUint8 之间的唯一区别在于如何处理溢出和下溢(如下一节所述)。建议避免使用前者 – 引用 Brendan Eich 的话

需要明确的是(我当时就在场),Uint8ClampedArray 完全 是一个历史遗留问题(来自 HTML5 canvas 元素)。除非您真的在做 canvas 相关的事情,否则请避免使用它。

20.2.2 处理溢出和下溢

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

无符号 8 位整数的模转换

> const uint8 = new Uint8Array(1);
> uint8[0] = 255; uint8[0] // highest value within range
255
> uint8[0] = 256; uint8[0] // overflow
0
> uint8[0] = 0; uint8[0] // lowest value within range
0
> uint8[0] = -1; uint8[0] // underflow
255

有符号 8 位整数的模转换

> const int8 = new Int8Array(1);
> int8[0] = 127; int8[0] // highest value within range
127
> int8[0] = 128; int8[0] // overflow
-128
> int8[0] = -128; int8[0] // lowest value within range
-128
> int8[0] = -129; int8[0] // underflow
127

钳制转换不同

> const uint8c = new Uint8ClampedArray(1);
> uint8c[0] = 255; uint8c[0] // highest value within range
255
> uint8c[0] = 256; uint8c[0] // overflow
255
> uint8c[0] = 0; uint8c[0] // lowest value within range
0
> uint8c[0] = -1; uint8c[0] // underflow
0

20.2.3 字节序

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

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

另一方面,协议和二进制文件的字节序各不相同,并且在不同平台上是固定的。因此,我们必须能够以任何一种字节序访问数据。DataViews 就用于这种情况,它允许您在获取或设置值时指定字节序。

引用维基百科关于字节序的解释:

您可以使用以下函数来确定平台的字节序。

const BIG_ENDIAN = Symbol('BIG_ENDIAN');
const LITTLE_ENDIAN = Symbol('LITTLE_ENDIAN');
function getPlatformEndianness() {
    const arr32 = Uint32Array.of(0x12345678);
    const arr8 = new Uint8Array(arr32.buffer);
    switch ((arr8[0]*0x1000000) + (arr8[1]*0x10000) + (arr8[2]*0x100) + (arr8\
[3])) {
        case 0x12345678:
            return BIG_ENDIAN;
        case 0x78563412:
            return LITTLE_ENDIAN;
        default:
            throw new Error('Unknown endianness');
    }
}

还有一些平台以与字内字节不同的字节序排列 (字节对)。这被称为混合字节序。如果您想支持这样的平台,那么很容易扩展前面的代码。

20.2.4 负索引

使用方括号运算符 [ ] 时,您只能使用非负索引(从 0 开始)。ArrayBuffers、类型化数组和 DataViews 的方法的工作方式不同:每个索引都可以为负数。如果是负数,则从长度开始倒数。换句话说,它被加到长度上以产生一个正常的索引。因此,-1 指的是最后一个元素,-2 指的是倒数第二个元素,等等。普通数组的方法的工作方式相同。

> const ui8 = Uint8Array.of(0, 1, 2);
> ui8.slice(-1)
Uint8Array [ 2 ]

另一方面,偏移量必须是非负数。例如,如果您将 -1 传递给

DataView.prototype.getInt8(byteOffset)

那么您将得到一个 RangeError

20.3 ArrayBuffers

ArrayBuffers 存储数据,视图(类型化数组和 DataViews)允许您读取和更改数据。为了创建 DataView,您需要为其构造函数提供一个 ArrayBuffer。类型化数组构造函数可以选择为您创建一个 ArrayBuffer。

20.3.1 ArrayBuffer 构造函数

构造函数的签名是

ArrayBuffer(length : number)

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

20.3.2 静态 ArrayBuffer 方法

20.3.3 ArrayBuffer.prototype 属性

20.4 类型化数组

各种类型化数组的区别仅在于其元素的类型

20.4.1 类型化数组与普通数组的比较

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

20.4.2 类型化数组是可迭代的

类型化数组实现了一个键为 Symbol.iterator 的方法,因此是可迭代的(有关更多信息,请参阅“可迭代对象和迭代器”一章)。这意味着您可以在 ES6 中使用 for-of 循环和类似机制

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

ArrayBuffers 和 DataViews 不可迭代。

20.4.3 类型化数组与普通数组之间的转换

要将普通数组转换为类型化数组,可以将其作为类型化数组构造函数的参数。例如

> const tarr = new Uint8Array([0,1,2]);

将类型化数组转换为数组的经典方法是在其上调用 Array.prototype.slice。这个技巧适用于所有类数组对象(例如 arguments),而类型化数组就是类数组。

> Array.prototype.slice.call(tarr)
[ 0, 1, 2 ]

在 ES6 中,您可以使用扩展运算符 (...),因为类型化数组是可迭代的

> [...tarr]
[ 0, 1, 2 ]

另一个 ES6 替代方法是 Array.from(),它适用于可迭代对象或类数组对象

> Array.from(tarr)
[ 0, 1, 2 ]

20.4.4 类型化数组的 Species 模式

有些方法会创建与 this 类似的新实例。Species 模式允许您配置应该使用哪个构造函数来执行此操作。例如,如果您创建了 Array 的子类 MyArray,则默认情况下 map() 会创建 MyArray 的实例。如果您希望它创建 Array 的实例,则可以使用 Species 模式来实现。详细信息在类一章的“Species 模式”一节中进行了解释。

ArrayBuffers 在以下位置使用 Species 模式

类型化数组在以下位置使用 Species 模式

DataViews 不使用 Species 模式。

20.4.5 类型化数组的继承层次结构

正如您在本章开头的图表中所见,所有类型化数组类(Uint8Array 等)都有一个共同的超类。我将该超类称为 TypedArray,但它不能从 JavaScript 直接访问(ES6 规范将其称为*内部对象 %TypedArray%*)。TypedArray.prototype 包含类型化数组的所有方法。

20.4.6 静态 TypedArray 方法

两个静态 TypedArray 方法都由其子类(Uint8Array 等)继承。

20.4.6.1 TypedArray.of()

此方法具有以下签名

TypedArray.of(...items)

它创建一个新的类型化数组,该数组是 this(调用 of() 的类)的实例。该实例的元素是 of() 的参数。

您可以将 of() 视为类型化数组的自定义字面量

> Float32Array.of(0.151, -8, 3.7)
Float32Array [ 0.151, -8, 3.7 ]
20.4.6.2 TypedArray.from()

此方法具有以下签名

TypedArray<U>.from(source : Iterable<T>, mapfn? : T => U, thisArg?)

它将可迭代对象 source 转换为 this(类型化数组)的实例。

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

> Uint16Array.from([0, 1, 2])
Uint16Array [ 0, 1, 2 ]

类型化数组也是可迭代的

> const ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));
> ui16 instanceof Uint16Array
true

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

  1. 不需要中间数组或类型化数组。
  2. 将类型化数组转换为元素精度更高的类型化数组时,映射步骤可以利用更高的精度。

为了说明第二个优点,让我们使用 map() 将类型化数组的元素加倍

> Int8Array.of(127, 126, 125).map(x => 2 * x)
Int8Array [ -2, -4, -6 ]

如您所见,这些值溢出并被强制转换为 Int8 值范围。如果通过 from() 进行映射,则可以选择结果的类型,以便值不会溢出

> Int16Array.from(Int8Array.of(127, 126, 125), x => 2 * x)
Int16Array [ 254, 252, 250 ]

Allen Wirfs-Brock 所说,类型化数组之间的映射是 from()mapfn 参数的动机。

20.4.7 TypedArray.prototype 属性

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

20.4.7.1 类型化数组特有的方法

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

20.4.7.2 数组方法

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

由于所有这些方法都可用于数组,因此您可以查阅以下两个来源以了解更多有关它们工作原理的信息

请注意,虽然普通数组方法是通用的(任何类数组 this 都可以),但本节中列出的方法不是(this 必须是类型化数组)。

20.4.8 «ElementType»Array 构造函数

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

每个构造函数都有五个重载版本 – 它的行为方式取决于它接收的参数数量及其类型。

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

const tarr1 = new Uint8Array([1,2,3]);

const tarr2 = Uint8Array.of(1,2,3);

const tarr3 = new Uint8Array(3);
tarr3[0] = 0;
tarr3[1] = 1;
tarr3[2] = 2;

20.4.9 静态 «ElementType»Array 属性

20.4.10 «ElementType»Array.prototype 属性

20.4.11 连接类型化数组

类型化数组没有像普通数组那样具有 concat() 方法。解决方法是使用方法

typedArray.set(arrayOrTypedArray, offset=0)

该方法将现有的类型化数组(或普通数组)复制到索引 offset 处的 typedArray 中。然后,您只需确保 typedArray 足够大以容纳要连接的所有(类型化)数组即可

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;
}
console.log(concatenate(Uint8Array,
    Uint8Array.of(1, 2), Uint8Array.of(3, 4)));
        // Uint8Array [1, 2, 3, 4]

20.5 DataView

20.5.1 DataView 构造函数

20.5.2 DataView.prototype 属性

20.6 支持类型化数组的浏览器 API

类型化数组已经存在一段时间了,因此有很多浏览器 API 支持它们。

20.6.1 文件 API

文件 API 允许您访问本地文件。以下代码演示了如何获取 ArrayBuffer 中提交的本地文件的字节。

const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function () {
    const arrayBuffer = reader.result;
    ···
};

20.6.2 XMLHttpRequest

在较新版本的 XMLHttpRequest API 中,您可以将结果以 ArrayBuffer 的形式传递

const xhr = new XMLHttpRequest();
xhr.open('GET', someUrl);
xhr.responseType = 'arraybuffer';

xhr.onload = function () {
    const arrayBuffer = xhr.response;
    ···
};

xhr.send();

20.6.3 Fetch API

XMLHttpRequest 类似,Fetch API 允许您请求资源。但它基于 Promise,这使得它更易于使用。以下代码演示了如何将 url 指向的内容作为 ArrayBuffer 下载

fetch(url)
.then(request => request.arrayBuffer())
.then(arrayBuffer => ···);

20.6.4 Canvas

引用 HTML5 规范:

canvas 元素为脚本提供了一个与分辨率相关的位图画布,可用于动态渲染图形、游戏图形、艺术品或其他视觉图像。

canvas 的 2D 上下文 允许您将位图数据检索为 Uint8ClampedArray 的实例

const canvas = document.getElementById('my_canvas');
const context = canvas.getContext('2d');
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
const uint8ClampedArray = imageData.data;

20.6.5 WebSockets

WebSockets 允许您通过 ArrayBuffers 发送和接收二进制数据

const socket = new WebSocket('ws://127.0.0.1:8081');
socket.binaryType = 'arraybuffer';

// Wait until socket is open
socket.addEventListener('open', function (event) {
    // Send binary data
    const typedArray = new Uint8Array(4);
    socket.send(typedArray.buffer);
});

// Receive binary data
socket.addEventListener('message', function (event) {
    const arrayBuffer = event.data;
    ···
});

20.6.6 其他 API

20.7 扩展示例:JPEG SOF0 解码器

该示例是一个网页,允许您上传 JPEG 文件并解析其结构以确定图像的高度和宽度等。

20.7.1 JPEG 文件格式

JPEG 文件是一系列(类型化数据)。每个段都以以下四个字节开头

JPEG 文件在所有平台上都是大端序的。因此,此示例演示了在使用 DataView 时指定字节序的重要性。

20.7.2 JavaScript 代码

以下函数 processArrayBuffer() 是实际代码的简化版本;为了减少混乱,我删除了一些错误检查。processArrayBuffer() 接收一个包含提交的 JPEG 文件内容的 ArrayBuffer,并迭代其段。

// JPEG is big endian
var IS_LITTLE_ENDIAN = false;

function processArrayBuffer(arrayBuffer) {
    try {
        var dv = new DataView(arrayBuffer);
        ···
        var ptr = 2;
        while (true) {
            ···
            var lastPtr = ptr;
            enforceValue(0xFF, dv.getUint8(ptr),
                'Not a marker');
            ptr++;
            var marker = dv.getUint8(ptr);
            ptr++;
            var len = dv.getUint16(ptr, IS_LITTLE_ENDIAN);
            ptr += len;
            logInfo('Marker: '+hex(marker)+' ('+len+' byte(s))');
            ···

            // Did we find what we were looking for?
            if (marker === 0xC0) { // SOF0
                logInfo(decodeSOF0(dv, lastPtr));
                break;
            }
        }
    } catch (e) {
        logError(e.message);
    }
}

此代码使用以下辅助函数(此处未显示)

decodeSOF0() 解析段 SOF0

function decodeSOF0(dv, start) {
    // Example (16x16):
    // FF C0 00 11 08 00 10 00 10 03 01 22 00 02 11 01 03 11 01
    var data = {};
    start += 4; // skip marker 0xFFC0 and segment length 0x0011
    var data = {
        bitsPerColorComponent: dv.getUint8(start), // usually 0x08
        imageHeight: dv.getUint16(start+1, IS_LITTLE_ENDIAN),
        imageWidth: dv.getUint16(start+3, IS_LITTLE_ENDIAN),
        numberOfColorComponents: dv.getUint8(start+5),
    };
    return JSON.stringify(data, null, 4);
}

有关 JPEG 文件结构的更多信息

20.8 可用性

许多类型化数组 API 都是由所有现代 JavaScript 引擎实现的,但有几个功能是 ECMAScript 6 中新增的

这些功能可能需要一段时间才能在所有地方都可用。像往常一样,kangax 的“ES6 兼容性表”描述了现状。

下一篇:21. 可迭代对象和迭代器