原文: http://exploringjs.com/impatient-js/ch_typed-arrays.html
Web 上的大量数据是文本:JSON 文件,HTML 文件,CSS 文件,JavaScript 代码等.JavaScript 通过其内置字符串很好地处理这些数据。
但是,在 2011 年之前,它没有很好地处理二进制数据。 2011 年 2 月 8 日推出了 Typed Array Specification 1.0 ,并提供了处理二进制数据的工具。使用 ECMAScript 6,将类型数组添加到核心语言中,并获得以前仅适用于普通数组(.map()
,.filter()
等)的方法。
Typed Arrays 的主要用例是:
以下浏览器 API 支持 Typed Arrays:
ArrayBuffer
,类型数组,DataView
Typed Array API 将二进制数据存储在ArrayBuffer
的实例中:
const buf = new ArrayBuffer(4); // length in bytes
// buf is initialized with zeros
ArrayBuffer 本身是不透明的。如果要访问其数据,则必须将其包装在另一个对象中 - _ 视图对象 _。有两种视图对象可供选择:
Uint8Array
:元素是无符号 8 位整数。 _ 无符号 _ 表示它们的范围从零开始。Int16Array
:元素是带符号的 16 位整数。 _ 签名 _ 意味着他们有一个标志,可以是负数,零或正数。Float32Array
:元素是 32 位浮点数。Uint8
,Int16
,Float32
等)。图 19 显示了 API 的类图。
Figure 19: The classes of the Typed Array API.
类型数组的使用方式与普通数组非常相似,但有一些显着差异:
这是使用类型化数组的示例:
const typedArray = new Uint8Array(2); // 2 elements
assert.equal(typedArray.length, 2);
// The wrapped ArrayBuffer
assert.deepEqual(
typedArray.buffer, new ArrayBuffer(2)); // 2 bytes
// Getting and setting elements:
assert.equal(typedArray[1], 0); // initialized with 0
typedArray[1] = 72;
assert.equal(typedArray[1], 72);
其他创建类型数组的方法:
const ta1 = new Uint8Array([5, 6]);
const ta2 = Uint8Array.of(5, 6);
assert.deepEqual(ta1, ta2);
这就是 DataViews 的使用方式:
const dataView = new DataView(new ArrayBuffer(4));
assert.equal(dataView.getUint16(0), 0);
assert.equal(dataView.getUint8(0), 0);
dataView.setUint8(0, 5);
API 支持以下元素类型:
元素类型 | 字节 | 描述 | C 型 |
---|---|---|---|
INT8 | 1 | 8 位有符号整数 | 签名的 char |
UINT8 | 1 | 8 位无符号整数 | 无符号的字符 |
Uint8C | 1 | 8 位无符号整数(钳位转换) | 无符号的字符 |
INT16 | 2 | 16 位有符号整数 | 短 |
UINT16 | 2 | 16 位无符号整数 | 未签约的短片 |
INT32 | 4 | 32 位有符号整数 | INT |
UINT32 | 4 | 32 位无符号整数 | unsigned int |
FLOAT32 | 4 | 32 位浮点 | 浮动 |
Float64 | 8 | 64 位浮点 | 双 |
元素类型Uint8C
是特殊的:DataView
不支持它,仅存在以启用Uint8ClampedArray
。这个类型数组由canvas
元素使用(它替换CanvasPixelArray
)。 Uint8C
和Uint8
之间的唯一区别是溢出和下溢的处理方式(如下一节所述)。建议避免前者 - 引用 Brendan Eich :
只是为了超级清晰(我出生时就在身边),
Uint8ClampedArray
完全是一个历史神器(HTML5 画布元素)。除非你真的在做帆布的事情,否则要避免。
通常,当值超出元素类型的范围时,使用模运算将其转换为范围内的值。对于有符号和无符号整数,这意味着:
以下功能有助于说明转换的工作原理:
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);
每当一个类型(例如Uint16
)被存储为多个字节的序列时, endianness 很重要:
Uint16
值 0x4321 存储为两个字节 - 首先是 0x43,然后是 0x21。Uint16
值 0x4321 存储为两个字节 - 首先是 0x21,然后是 0x43。Endianness 往往是针对每个 CPU 架构固定的,并且在本机 API 之间保持一致。类型化数组用于与这些 API 通信,这就是为什么它们的字节顺序遵循平台的字节顺序而无法更改。
另一方面,协议和二进制文件的字节顺序各不相同,并且在不同平台上是固定的。因此,我们必须能够以任何字节顺序访问数据。 DataViews 提供此用例,并允许您在获取或设置值时指定字节序。
您可以使用以下函数来确定平台的字节顺序。
const BIG_ENDIAN = Symbol('BIG_ENDIAN');
const LITTLE_ENDIAN = Symbol('LITTLE_ENDIAN');
function getPlatformEndianness() {
const arr32 = Uint32Array.of(0x87654321);
const arr8 = new Uint8Array(arr32.buffer);
if (compare(arr8, [0x87, 0x65, 0x43, 0x21])) {
return BIG_ENDIAN;
} else if (compare(arr8, [0x21, 0x43, 0x65, 0x87])) {
return LITTLE_ENDIAN;
} else {
throw new Error('Unknown endianness');
}
}
function compare(arr1, arr2) {
if (arr1.length !== arr2.length) {
return false;
}
for (let i=0; i<arr1.length; i++) {
if (arr1[i] !== arr2[i]) {
return false;
}
}
return true;
}
其他排序也是可能的。这些通常被称为 _ 中端 _ 或 _ 混合端 _。
对于 Typed Arrays,我们区分:
括号运算符的指数[ ]
:您只能使用非负指数(从 0 开始)。
ArrayBuffers,Typed Arrays 和 DataViews 方法的索引:每个索引都可以是负数。如果是,则将其添加到实体的长度,以生成实际索引。因此,-1
指的是最后一个元素,-2
指向倒数第二个等。正常数组的方法以相同的方式工作。
const ui8 = Uint8Array.of(0, 1, 2);
assert.deepEqual(ui8.slice(-1), Uint8Array.of(2));
传递给 Typed Arrays 和 DataViews 方法的偏移量:必须是非负数。例如:
const dataView = new DataView(new ArrayBuffer(4));
assert.throws(
() => dataView.getUint8(-1),
{
name: 'RangeError',
message: 'Offset is outside the bounds of the DataView',
});
ArrayBuffers 存储二进制数据,可通过 Typed Arrays 和 DataViews 访问。
new ArrayBuffer()
构造函数的类型签名是:
new ArrayBuffer(length: number)
通过new
调用此构造函数会创建一个容量为length
字节的实例。每个字节最初为 0。
您无法更改 ArrayBuffer 的长度,您只能创建一个具有不同长度的新长度。
ArrayBuffer
的静态方法ArrayBuffer.isView(arg: any)
如果arg
是对象,则返回true
,返回 ArrayBuffer(Typed Array 或 DataView)的视图。ArrayBuffer.prototype
的属性get .byteLength(): number
以字节为单位返回此 ArrayBuffer 的容量。
.slice(startIndex: number, endIndex=this.byteLength)
创建一个新的 ArrayBuffer,其中包含此 ArrayBuffer 的字节,其索引大于或等于startIndex
且小于endIndex
。 start
和endIndex
可以是负数(参见“指数和偏移”部分)。
各种类型的数组只是不同的 w.r.t.他们的元素类型:
Int8Array
,Uint8Array
,Uint8ClampedArray
,Int16Array
,Uint16Array
,Int32Array
,Uint32Array
Float32Array
,Float64Array
类型数组与普通数组非常相似:它们有.length
,元素可以通过括号运算符[ ]
访问,并且它们具有大多数标准数组方法。它们在以下方面与普通数组不同:
Typed Arrays 有缓冲区。类型数组ta
的元素不存储在ta
中,它们存储在可通过ta.buffer
访问的关联 ArrayBuffer 中:
const ta = new Uint8Array(4); // 4 elements
assert.deepEqual(
ta.buffer, new ArrayBuffer(4)); // 4 bytes
* 使用零初始化类型化数组:
* `new Array(4)`创建一个没有任何元素的普通数组。它只有 4 个 _ 孔 _(小于`.length`的指数没有相关元素)。
* `new Uint8Array(4)`创建一个 Typed Array,其中 4 个元素都为 0。
```js
assert.deepEqual(new Uint8Array(4), Uint8Array.of(0, 0, 0, 0));
```
* Typed Array 的所有元素都具有相同的类型:
* 设置元素将值转换为该类型。
```js
const ta = new Uint8Array(1);
ta[0] = 256;
assert.equal(ta[0], 0);
ta[0] = '2';
assert.equal(ta[0], 2);
```
* 获取元素返回数字。
```js
const ta = new Uint8Array(1);
assert.equal(ta[0], 0);
assert.equal(typeof ta[0], 'number');
```
* Typed Array 的`.length`派生自其 ArrayBuffer 并且永远不会更改(除非您切换到不同的 ArrayBuffer)。
* 普通数组可以有孔;键入的数组不能。
#### 29.4.2。类型化数组是可迭代的
类型数组是[可迭代的](ch_sync-iteration.html)。这意味着您可以使用`for-of`循环和类似的机制:
```js
const ui8 = Uint8Array.of(0,1,2);
for (const byte of ui8) {
console.log(byte);
}
// Output:
// 0
// 1
// 2
ArrayBuffers 和 DataViews 不可迭代。
要将普通数组转换为类型化数组,请将其传递给类型化数组构造函数。例如:
const tarr = new Uint8Array([0,1,2]);
要将 Typed Array 转换为普通 Array,可以使用 spread 或Array.from()
(因为 Typed Arrays 是可迭代的):
assert.deepEqual([...tarr], [0,1,2]);
assert.deepEqual(Array.from(tarr), [0,1,2]);
各种 Typed Array 对象的属性分两步介绍:
TypedArray
:首先,我们看一下所有 Typed Array 类的公共超类(在本章开头的类图中显示)。我正在调用超类TypedArray
,但它不能直接从 JavaScript 访问。 TypedArray.prototype
包含 Typed Arrays 的所有方法。«ElementType»Array
:实际的 Typed Array 类称为Uint8Array
,Int16Array
,Float32Array
等。TypedArray
的静态方法静态TypedArray
方法都由其子类(Uint8Array
等)继承。
TypedArray.of()
此方法具有类型签名:
.of(...items: number[]): instanceof this
返回类型的表示法是我的发明:.of()
返回this
的实例(调用of()
的类)。实例的元素是of()
的参数。
您可以将of()
视为 Typed Arrays 的自定义字面值:
const float32Arr = Float32Array.of(0.151, -8, 3.7);
const int16Arr = Int32Array.of(-10, 5, 7);
TypedArray.from()
此方法具有类型签名:
TypedArray<T>.from<S>(
source: Iterable<S>|ArrayLike<S>, mapfn?: S => T, thisArg?: any)
: instanceof this
它将source
转换为this
(类型化数组)的实例。再一次,语法instanceof 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()
单独映射相比,有两个优点:
为了说明第二个优点,我们首先将 Typed 数组转换为具有更高精度的 Typed 数组。如果我们使用.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()
进行映射,那么输入类型和输出类型是相同的(如果我们使用 Typed Arrays)。相反,.from()
从任意输入类型变为您通过其接收器指定的输出类型。
根据 Allen Wirfs-Brock ,Typed Arrays 之间的映射是.from()
的mapfn
参数的动机。
TypedArray<T>.prototype
的属性Typed Array 方法接受的索引可能是否定的(它们就像传统的 Array 方法一样)。偏移必须是非负的。有关详细信息,请参见“指数和偏移”部分。
以下属性特定于 Typed Arrays;普通数组没有它们:
get .buffer(): ArrayBuffer
返回支持此 Typed Array 的缓冲区。
get .length(): number
返回此 Typed Array 缓冲区元素的长度。请注意,普通数组的长度不是 getter,它是实例具有的特殊属性。
get .byteLength(): number
返回此 Typed Array 缓冲区的大小(以字节为单位)。
get .byteOffset(): number
返回此 Arrayd Array 在其 ArrayBuffer 中“启动”的偏移量。
.set(arrayLike: ArrayLike<number>, offset=0): void
.set(typedArray: TypedArray, offset=0): void
将第一个参数的所有元素复制到此 Typed 数组。参数索引 0 处的元素被写入此类型数组的索引offset
(等)。
arrayLike
:它的元素被转换为数字,然后转换为此类型数组的元素类型T
。typedArray
:它的每个元素都直接转换为此类型数组的相应类型。如果两个 Typed Arrays 具有相同的元素类型然后更快,则使用逐字节复制。.subarray(startIndex=0, end=this.length): TypedArray<T>
返回一个新的 Typed Array,它与此 Typed Array 具有相同的缓冲区,但是(通常)较小的范围。如果startIndex
为非负数,则生成的类型化数组的第一个元素为this[startIndex]
,第二个this[startIndex+1]
(等)。如果startIndex
为负数,则进行适当转换。
以下方法与普通数组的方法基本相同:
.copyWithin(target: number, start: number, end=this.length): this
[W,ES6].entries(): Iterable<[number, T]>
[R,ES6].every(callback: (value: T, index: number, array: TypedArray<T>) => boolean, thisArg?: any): boolean
[R,ES5].fill(value: T, start=0, end=this.length): this
[W,ES6].filter(callback: (value: T, index: number, array: TypedArray<T>) => any, thisArg?: any): T[]
[R,ES5].find(predicate: (value: T, index: number, obj: T[]) => boolean, thisArg?: any): T | undefined
[R,ES6].findIndex(predicate: (value: T, index: number, obj: T[]) => boolean, thisArg?: any): number
[R,ES6].forEach(callback: (value: T, index: number, array: TypedArray<T>) => void, thisArg?: any): void
[R,ES5].includes(searchElement: T, fromIndex=0): boolean
[R,ES2016].indexOf(searchElement: T, fromIndex=0): number
[R,ES5].join(separator = ','): string
[R,ES1].keys(): Iterable<number>
[R,ES6].lastIndexOf(searchElement: T, fromIndex=this.length-1): number
[R,ES5].map<U>(mapFunc: (value: T, index: number, array: TypedArray<T>) => U, thisArg?: any): U[]
[R,ES5].reduce<U>(callback: (accumulator: U, element: T, index: number, array: T[]) => U, init?: U): U
[R,ES5].reduceRight<U>(callback: (accumulator: U, element: T, index: number, array: T[]) => U, init?: U): U
[R,ES5].reverse(): this
[W,ES1].slice(start=0, end=this.length): T[]
[R,ES3].some(callback: (value: T, index: number, array: TypedArray<T>) => boolean, thisArg?: any): boolean
[R,ES5].sort(compareFunc?: (a: T, b: T) => number): this
[W,ES1].toString(): string
[R,ES1].values(): Iterable<number>
[R,ES6]有关这些方法如何工作的详细信息,请参阅关于普通数组的章节。
new «ElementType»Array()
每个 Typed Array 构造函数的名称都遵循模式«ElementType»Array
,其中«ElementType»
是开头表格中的元素类型之一。这意味着有 9 个类型数组的构造函数:Int8Array
,Uint8Array
,Uint8ClampedArray
(元素类型Uint8C
),Int16Array
,Uint16Array
,Int32Array
,Uint32Array
,Float32Array
, Float64Array
。
每个构造函数都有四个 _ 重载 _ 版本 - 它的行为会有所不同,具体取决于它接收的参数数量以及它们的类型:
new «ElementType»Array(buffer: ArrayBuffer, byteOffset=0, length=0)
创建一个新的«ElementType»Array
,其缓冲区为buffer
。它开始访问给定byteOffset
的缓冲区,并具有给定的length
。请注意,length
计算 Typed Array 的元素(每个 1-4 字节),而不是字节。
new «ElementType»Array(length=0)
使用给定的length
和相应的缓冲区(其大小以字节为length * «ElementType»Array.BYTES_PER_ELEMENT
)创建新的«ElementType»Array
。
new «ElementType»Array(source: TypedArray)
创建«ElementType»Array
的新实例,其元素与source
的元素具有相同的值,但强制为ElementType
。
new «ElementType»Array(source: ArrayLike<number>)
创建«ElementType»Array
的新实例,其元素与source
的元素具有相同的值,但强制为ElementType
。 (有关类似数组的对象的更多信息,请参阅关于数组的章节。)
以下代码显示了创建相同 Typed 数组的三种不同方法:
const ta1 = new Uint8Array([0, 1, 2]);
const ta2 = Uint8Array.of(0, 1, 2);
const ta3 = new Uint8Array(3);
ta3[0] = 0;
ta3[1] = 1;
ta3[2] = 2;
assert.deepEqual(ta1, ta2);
assert.deepEqual(ta1, ta3);
«ElementType»Array
的静态属性«ElementType»Array.BYTES_PER_ELEMENT: number
计算存储单个元素需要多少字节:
> Uint8Array.BYTES_PER_ELEMENT
1
> Int16Array.BYTES_PER_ELEMENT
2
> Float64Array.BYTES_PER_ELEMENT
8
«ElementType»Array.prototype
的属性.BYTES_PER_ELEMENT: number
与«ElementType»Array.BYTES_PER_ELEMENT
相同。
类型数组没有方法.concat()
,就像普通数组那样。解决方法是使用该方法
.set(typedArray: TypedArray, offset=0): void
该方法将现有的 Typed Array 复制到索引offset
的typedArray
中。然后你只需要确保typedArray
足够大以容纳你想要连接的所有(Typed)数组:
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)
);
new DataView()
new DataView(buffer: ArrayBuffer, byteOffset=0, byteLength=buffer.byteLength-byteOffset)
创建一个新的 DataView,其数据存储在 ArrayBuffer buffer
中。默认情况下,新的 DataView 可以访问所有buffer
,最后两个参数允许您更改它。
DataView.prototype
的属性«ElementType»
可以是:Float32
,Float64
,Int8
,Int16
,Int32
,Uint8
,Uint16
,Uint32
。
get .buffer()
返回此 DataView 的 ArrayBuffer。
get .byteLength()
返回此 DataView 可以访问的字节数。
get .byteOffset()
返回此 DataView 开始访问其缓冲区中的字节的偏移量。
.get«ElementType»(byteOffset: number, littleEndian=false)
从此 DataView 的缓冲区中读取值。
.set«ElementType»(byteOffset: number, value: number, littleEndian=false)
将value
写入此 DataView 的缓冲区。
“探索 ES6”中关于类型化数组的章节还有一些额外的内容: