类型化数组
JavaScript 中的普通数组是非类型化的,即数组中可以存放任意类型的元素,且一个数组中的多个元素也可以是不同的类型。
相反,类型化数组实例有一个固定的类型,其中只能存储相应类型的元素。目前 JavaScript 中支持的类型化数组如下:
整型类型化数组
类型 | 符号 | 元素字节长度 |
---|---|---|
Int8Array | 有 | 1 |
Uint8Array | 无 | 1 |
Uint8ClampedArray | 无 | 1 |
Int16Array | 有 | 2 |
Uint16Array | 无 | 2 |
Int32Array | 有 | 4 |
Uint32Array | 无 | 4 |
BigInt64Array | 有 | 8 |
BigUint64Array | 无 | 8 |
浮点类型化数组
类型 | 元素字节长度 |
---|---|
Float16Array | 2 |
Float32Array | 4 |
Float64Array | 8 |
作为视图的类型化数组
类型化数组是 ArrayBuffer 的上层视图,通过类型化数组可以查看和修改其底层 ArrayBuffer 的字节数据。
同一个 ArrayBuffer,使用不同的类型化数组去解读或修改,会得到不同的结果。以下示例展示了Uint8Array
和Uint16Array
对同一个 ArrayBuffer 的不同解读,并假设数值为 16 进制:
ArrayBuffer(byteLength=8) | 00 01 02 03 04 05 06 07 |
Uint8Array(length=8) | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 |
Uint16Array(length=4) | 00 01 | 02 03 | 04 05 | 06 07 |
上例中Uint8Array
将底层 ArrayBuffer 的每一个字节解读为一个元素,而Uint16Array
却将连续的两个字节解读为一个元素。当我们通过下标访问Uint8Array
中的元素时,其返回 ArrayBuffer 中具体字节的值;而通过Uint16Array
访问某个下标时,会取出对应位置连续的两个字节,把它当作一个无符号 16 位整型值返回。例如以array[1]
的形式访问Uint16Array
,在小端序的架构上返回的值代表0x0302
。
创建类型化数组
可以传递已有的 ArrayBuffer 给类型化数组的构造函数,以使该类型化数组使用传入的 ArrayBuffer 作为底层数据。
除此之外,通常在创建类型化数组的同时创建新的 ArrayBuffer,作为底层数据使用。
通过已有的 ArrayBuffer 创建
语法:
new TypedArray(buffer);
new TypedArray(buffer, byteOffset);
new TypedArray(buffer, byteOffset, length);
当通过已有的 ArrayBuffer 创建类型化数组时,构造函数不再单独创建新的 ArrayBuffer,而是将传入的 ArrayBuffer 作为底层缓冲区使用。可以通过一个 ArrayBuffer 构造多个不同的类型化数组,它们将共享相同的底层数组。
还可以额外指定要创建的数组所使用的 ArrayBuffer 的起始位置(偏移量)和长度(元素个数),这两个参数都是可选的。
const buffer = new ArrayBuffer(16);
const u8 = new Uint8Array(buffer);
u8.forEach((v, i) => {
u8[i] = i;
});
[...u8].map(v => v.toString(16));
// ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
const u16 = new Uint16Array(buffer);
[...u16].map(v => v.toString(16).padStart(4, "0"));
// 小端序架构输出:['0100', '0302', '0504', '0706', '0908', '0b0a', '0d0c', '0f0e']
const i16 = new Int16Array(buffer, 2, 4);
[...i16].map(v => v.toString(16).padStart(4, "0"));
// 小端序架构输出:['0302', '0504', '0706', '0908']
通过普通数组初始化
可通过非类型化数组的值来初始化类型化数组,超出元素长度的值会被截断,构造函数会自动创建对应的底层 ArrayBuffer:
const arrI8 = new Int8Array([0, 1, 2, 3, 32767, 32768]);
arrI8; // [0, 1, 2, 3, -1, 0]
通过已有的类型化数组创建新的类型化数组
通过已有的类型化数组创建新的类型化数组,效果与通过普通数组初始化相同,即按元素复制值而不是按字节数据复制。
const arrU16 = new Uint16Array([65535, 65536]);
const arrI8 = new Int8Array(arrU16);
arrI8; // [-1, 0]
通过指定数组长度创建
可以指定数组的元素个数,他们将被初始化为0
:
const arr16 = new Int16Array(3);
arr16; // [0, 0, 0]
通过迭代器创建
将迭代器传入构造函数,可以创建包含迭代器返回值的类型化数组。
function* threeNumbers() {
yield 10;
yield 20;
yield 30;
}
const u8 = new Uint8Array(threeNumbers());
u8; // [10, 20, 30]
使用TypedArray.from()
通过可迭代对象或类数组创建
语法:
TypedArray.from(list);
TypedArray.from(list, mapFn);
TypedArray.from(list, mapFn, thisArg);
如果只传入第一个参数,大部分情况下,该方法与将普通数组或类型化数组传入构造函数效果相同,但该方法有一个特别的用法是可以分解字符串值,因为字符串也是可迭代对象。
Uint8Array.from([1, 2, 3]); // [1, 2, 3]
Uint16Array.from("123456"); // [1, 2, 3, 4, 5, 6]
可以在创建时同时指定第二个参数(map 函数)对输入数据做变换:
const source = [1, 2, 3];
const dest = Int16Array.from(source, n => n * n);
dest; // [1, 4, 9]
使用TypedArray.of()
通过枚举列表创建
Uint8Array.of(10, 20, 30); // [10, 20, 30]
const primes = [2n, 3n, 5n, 7n, 11n];
BigUint64Array.of(...primes); // [2n, 3n, 5n, 7n, 11n]
代表 ArrayBuffer 视图
静态方法ArrayBuffer.isView()
可以验证某个对象是不是 ArrayBuffer 的视图。由于类型化数组是 ArrayBuffer 的视图,所以会返回true
。
ArrayBuffer.isView(new Int8Array(1)); // true
ArrayBuffer.isView(new Float32Array(5)); // true
ArrayBuffer.isView([]); //false
通用属性
静态属性
静态属性BYTES_PER_ELEMENT
指示该种类型化数组每个元素占用的字节长度。
Int8Array.BYTES_PER_ELEMENT; // 1
Int16Array.BYTES_PER_ELEMENT; // 2
Float32Array.BYTES_PER_ELEMENT; // 4
BigUint64Array.BYTES_PER_ELEMENT; // 8
实例属性
length
指示数组长度(元素个数),而byteLength
指示数组的字节长度:
const u32 = new Uint32Array(10);
u32.length; // 10
u32.byteLength; // 40
byteOffset
指示数组的开头在底层 ArrayBuffer 中的偏移量:
const buffer = new ArrayBuffer(16);
const u8 = new Uint8Array(buffer);
u8.forEach((v, i) => {
u8[i] = i;
});
const bytes = new Uint8Array(buffer, 2, 5);
bytes; // [2, 3, 4, 5, 6]
bytes.byteOffset; // 2
通过buffer
属性可以访问底层 ArrayBuffer:
const u8 = new Uint8Array(2);
u8[1] = 255;
const u16 = new Uint16Array(u8.buffer);
u16[0].toString(16); // ff00(小端序)
通用方法
类型化数组有一些普通数组也有的通用方法,例如forEach
、find
、indexOf
等,此处不再赘述。
map()
返回同类型数组
类型化数组的map
方法在对现有数组元素执行映射逻辑后,返回新的相同类型的类型化数组:
const u8 = new Uint8Array([1, 2, 3, 4, 5]);
const mapped = u8.map(n => n * 10);
mapped; // [10, 20, 30, 40, 50]
mapped instanceof Uint8Array; // true
set()
:从其它数组写入连续元素
语法:
typedArray.set(sourceArray);
typedArray.set(sourceArray, targetOffset);
其中sourceArray
既可以是普通数组,也可以是类型化数组,可选的targetOffset
指定写入目标数组typedArray
的起始位置:
const bytes = new Uint8Array(8);
bytes.set([1, 2]);
bytes; // [1, 2, 0, 0, 0, 0, 0, 0]
bytes.set([3, 4], 3);
bytes; // [1, 2, 0, 3, 4, 0, 0, 0];
const list = new Int16Array([7, 8]);
bytes.set(list, 6);
bytes; // [1, 2, 0, 3, 4, 0, 7, 8]
subarray()
:取子数组
语法:
typedArray.subarray();
typedArray.subarray(begin);
typedArray.subarray(begin, end);
从typedArray
取子数组,两个参数分别是开始位置和结束位置。新数组和原数组共享相同的 ArrayBuffer,但各自可以有不同的byteOffset
。
两个位置参数都可以是负数,代表从数组末尾开始倒数的位置。
const list = new Int32Array([1, 2, 3, 4, 5, 6]);
let subList;
subList = list.subarray();
subList; // [1, 2, 3, 4, 5, 6]
subList.buffer === list.buffer; // true
subList.byteOffset; // 0
subList = list.subarray(2);
subList; // [3, 4, 5, 6]
subList.buffer === list.buffer; // true
subList.byteOffset; // 8
subList = list.subarray(2, 5);
subList; // [3, 4, 5]
subList.buffer === list.buffer; // true
subList.byteOffset; // 8
subList = list.subarray(-4, -1);
subList; // [3, 4, 5]
subList.buffer === list.buffer; // true
subList.byteOffset; // 8
如需要产生副本子数组,即原数组和子数组不共享同一个 ArrayBuffer,可以换用slice
方法:
const list = new Int32Array([1, 2, 3, 4, 5, 6]);
const copied = list.slice(2);
copied; // [3, 4, 5, 6]
copied.buffer === list.buffer; // false