定型数组(typed array) 是 ECMAScript 新增的结构,目的是提升向原生库传输数据的效率,但是 javascript 没有 “TypedArray” 类型,它所指的其实是一种特殊的包含数值类型的数组
为啥会出现定型数组?
浏览器为了支持复杂的图形渲染,出现了 WebGL,因为 Js 数组于原生数组之间不匹配所以出现了性能问题,图形驱动程序 API 通常不需要以 JS 默认双精度格式(js 数据在内存中的格式)传递给他们的数值,而每次 WebGL 与 JS 运行时之间传递数组时,WebGL 绑定都需要在目标环境分配新数组,以其当前格式迭代数组,然后将数值类型转为新数组中的适当格式,而这些要花费更多时间。目的就是提高与 WebGL 等原生库交换二进制数据的效率。
ArrayBuffer
ShearedArrayBuffer 是 ArrayBuffer 的一个变体,可以无需复制就在执行上下文间传递他
Float32Array 实际上是一种“视图”,可以允许 js 运行时访问一块名为 ArrayBuffer 的预分配内存。ArrayBuffer 是所有定型数组及试图引用的基本单位。
ArrayBuffer 是一个普通的 JS 构造函数,可用于在内存中分配特定数量的字节空间, 一经创建就不能再调整大小了,即不能直接读取或写入,却可以使用 slice()复制其全部或部分到一个新实例中, 要读取或写入 ArrayBuffer 就必须通过视图,视图有不同的类型,但是引用的都是 ArrayBuffer 中存储的二进制数据
const buf = new ArrayBuffer(16); // 在内存中分配 16 字节
log(buf.byteLength); // log: 16
const bufSlice = buf.slice(4,12);
log(bufSlice); // 8
ArrayBuffer 某种程度上类似于 C++ 的 malloc(),区别如下:
-
- malloc 在分配失败时会返回一个 null 指针,ArrayBuffer 在分配失败时会抛出错误
- malloc 可以利用虚拟内存,因此最大可分配尺寸只受可寻址系统内存的限制,ArrayBuffer 分配的内存不能超过 Number.MAX_SAFE_INTEGER 字节
- malloc 调用成功不会出实话实际的地址,声明 ArrayBuffer 则会将所有二进制初始化为 0
- malloc 分配的堆内存除非调用 free 或程序退出,否则系统不能再使用;而通过声明 ArrayBuffer 分配的堆内存可以被当成垃圾回收,不需要手动释放
允许读写 ArrayBuffer 的视图
DataView
第一种允许读写 ArrayBuffer 的视图就是 DataView, 这个视图为文件 I/O 和 网络 I/O 设计,其 API 支持对缓冲数据的高度控制,但相比于其他类型的视图性能也差一些,DataView 对缓冲内容没有任何预设,也不能迭代
必须对已有的 ArrayBuffer 读取或写入时才能创建 DataView 实例(可以使用 全部或部分 ArrayBuffer),DataView 的实例且维护着对该缓冲实例的引用(ArrayBuffer 的返回实例),以及视图在缓冲中开始的位置。
const buf = new ArrayBuffer(16);
// DataView 试图默认使用整个 ArrayBuffer
/**
* TS 声明 DataView: (buffer: ArrayBufferLike, byteOffset?: number | undefined, byteLength?: number | undefined) => DataView
* 表明 第一个参数为 定型数组 类ArrayBuffer类型,byteOffset 表示字节偏移量, byteLength 表示子节长度
*/
const fullDataView = new DataView(buf);
log("偏移量:", fullDataView.byteOffset, " 字节长度:", fullDataView.byteLength); // 偏移量: 0 字节长度: 16
log(fullDataView.buffer === buf); // log: true
const firstHalfDV = new DataView(buf, 8, 8); // 创建一个 对 ArrayBuffer 从 8 开始,长度为 8 的试图
log(firstHalfDV.byteLength); // log: 8
要通过 DataView 读取缓冲 (bufferArray) 还需要几个组件
-
- 首先是要读或写的字节偏移量(byteOffset)。可以看成 DataView 中的某种“地址”
- DataView 应该使用 ElementType 来实现 js 的 Number 类型到缓冲内存二进制格式的转换
- 最后是内存中值的字节序,默认为大端字节序
ElementType
DataView 暴露的 API 强制开发者在读、写时指定一个 ElementType,然后 DataView 为读、写完成相应的转换
一个整数四个字节;十六进制,一个字节刚好装两个数,在计算机内部地址是使用16进制表示的
ECMAScript 6 支持 8 种不同的 ElementType,如下表,
| ElementType | 字节 | 说明 |
|---|---|---|
| Int8 | 1 | 8 位有符号整数 |
| Uint8 | 1 | 8 位无符号整数 |
| Int16 | 2 | 16位有符号整数 |
| Int32 | 4 | 32 位有符号整数 |
| Uint32 | 4 | 32 位无符号整数 |
| Float32 | 4 | 32位 IEE754 浮点数 |
| Float64 | 8 | 64 位 IEE754 浮点数 |
DataView 为上述每种类型都暴露了 get 和 set 方法,这些方法使用 byteOffset (字节偏移量)定位要读取 或 写入值的位置,类型是可以互相使用的(不同的类型会自动进行转换)
// 在内存中分配两个字节并声明一个 DataView
const buf = new ArrayBuffer(2); // 声明 ArrayBuffer 则会将所有二进制初始化为 0
const view = new DataView(buf);
// 检查第一个 和 第二个字符
log(view.getInt8(0), view.getInt8(1)); // log: 0, 0
// 将整个缓冲都设置为 1
// 255 的二进制是 1111 1111 (2**8 -1),16进制是: (255).toString(16) = ff (即 0xFF)
// setUint8(byteOffset: number, value: number): void;
view.setUint8(0, 255); // Dataview 会自动将数据转化为 特定的 ElementType,现在 缓冲里边都是 1 了
log(view.getUint8(0)); // 255
log(view.getInt16(0)); // -256, 为啥呀?
字节序
字节序 指的是计算机系统维护的一种字节顺序的约定,DataView 只支持两种约定: 大端字节序 和 小端字节序。 大端字节序(Big Endian)也称为“网络字节序”,意思是最高有效位保留在第一个字节,而最低有效位保留在最后一个字节(高位字节数据存放在内存低地址处,低位字节数据存放在内存高地址处);小端字节序(Little Endian)高位字节数据存放在内存高地址处,低位数据存放在内存低地址处
参考链接: blog.csdn.net/damanchen/a…
js 运行时所在系统的原生字节序决定了如何读取或写入字节,但 DataView 并不遵守整个约定,对于一段内存而言,DataView 是一个中立接口,它会遵守你指定的字节序。DataView 的所有 API 方式都以大端字节序作为默认值,但接受一个可选的布尔值参数,设置为 true 即可开启小端字节序
const buf = new ArrayBuffer(2); // 在内存中分配 2 字节,一个字节等于 8 位
const view = new DataView(buf);
view.setUint8(0, 0x80); // 设置最左边的位等于 1, (0x80).toString(2) = 1000 0000(2进制)
view.setUint8(1, 0x01); // 设置最右边的位等于 1, 0x01 = 0000 0001(二进制)
// 缓冲内容
// 0x8 0x0 0x0 0x1
// 1000 0000 0000 0001
// 按打断字节序读取 Unit16
// 0x80 是 高字节, 0x01 是低字节
// 0x8001 = 2**15 + 2**0 = 32768 + 1 = 32769
log(view.getUint16(0)); // log: 32769
// 按照小端字节序读取 Unit16
// 0x01 是高字节, 0x01 是低字节
// 0x0180 = 2**8 + 2**7 = 256 + 128 = 384
log(view.getUint16(0, true)); // log: 384
// 按大端字节序读取 Unit16
view.setUint16(0, 0x0004);
// 缓冲内容为
// 0x0 0x0 0x0 0x4
// 0000 0000 0000 0100
log(view.getUint8(0)); // 0, 对应 B0000 0000 = 0
log(view.getUint8(1)); // 4, 对应 B0000 0100 = 4
// 按小端字节序写入 Unit16
view.setUint16(0, 0x0002, true);
// 缓冲内容为
// 0x0 0x2 0x0 0x0
// 0000 0010 0000 0000
log(view.getUint8(0)); // 2, 对应 B0000 0010 = 2
log(view.getUint8(1)); // 0,对应 B0000 0000 = 0
DataView 的边界
DataView 完成读、写操作的前提是必须有充足的缓冲区,否则会抛出 RangeError
const buf = new ArrayBuffer(6); // 在内存中分配 6 字节
const view = new DataView(buf);
// 尝试读取超出缓冲范围的值
// 一个 Int32 对应 4 个字节,偏移位 4 + 4 = 8 > 6 超出了范围
view.getInt32(4); // RangeError: Offset is outside the bounds of the DataView
DataView 在写入缓冲里会尽最大努力把一个值转化为适当的类型,后备为 0,如果无法转换,则抛出错误
定型数组
定型数组是另一种形式的 ArrayBuffer 视图,虽然概念上与 DataView 接近,但是定型数组特定于一种 ElementType 且遵循系统原生的字节序。创建定型数组的方式包括读取已有缓冲,使用自有缓冲、填充可迭代结构,以及填充基于任意类型的定型数组,通过 .from() 和 .of() 也可以创建定型数组
// 创建一个 12 字节的缓冲
const buf = new ArrayBuffer(12);
// 创建一个 引用该缓冲的 Int32Array, Int32 每个元素需要 4 字节,因此返回的实例的长度 为 3
const ints = new Int32Array(buf);
log(ints.length); // log: 3
// 创建一个长度为 6 的 Int32Array
// ts: new (length: number) => Int32Array,i32 每个数值使用 4 字节,因此 ArrayBuffer 是 24 字节
const i32a = new Int32Array(6);
log("length: ", i32a.length); // log: 6
log("buffer length: ", i32a.buffer.byteLength); // 24
// 创建一个包含 [2,4,6,8] 的 Int32Array
const i32a2 = new Int32Array([2, 4, 6, 8]);
log("i32a2 len: ", i32a2.length); // log: 4
log("index 2: ", i32a2[2]); // log: 6
// 通过复制 i32a2 的值创建一个 Int16Array, 这个新类型数组会分配自己的缓冲,对应索引的每个值会相应地转换为新格式
// i16 每个数值使用 2 字节
log(Int16Array.BYTES_PER_ELEMENT); // log: 2
const i32aClone = new Int16Array(i32a2);
log(i32aClone.length); // log: 4
log(i32aClone.buffer.byteLength); // log: 8
定型构造函数和实例都有一个 BYTES\_PER\_ELEMENT 属性返回该类型数组中每个元素的大小, 如果定型数组没有任何初始化,则其关联的缓冲会以 0 填充, 超过边界的值为 undefined
log(Int8Array.BYTES_PER_ELEMENT); // 1
log(Int16Array.BYTES_PER_ELEMENT); // 2
log(Int32Array.BYTES_PER_ELEMENT); // 4
const i8V = new Int8Array(3); // 创建长度为 3 的 Int8Array 实例
log(i8V.BYTES_PER_ELEMENT); // 1
log(i8V[0]); // 0
log(i8V[1]); // 0
log(i8V[2]); // 0
log(i8V[3]); // undefined
log(i8V[4]); // undefiend
在大多数场景下,定型数组和普通数组拥有相同的方法和操作符、属性
const i16a = new Int16Array([1, 2, 3]);
// 返回新数组的方式也会返回包含同样元素的类型(element type) 的新定型数组
const doubleI16a = i16a.map((x) => 2 * x);
log(doubleI16a); // Int16Array(3) [ 2, 4, 6 ]
log(doubleI16a instanceof Int16Array); // true
定型数组有一个 Symbol.iterator 符号属性,因此可以通过 for..of 循环和扩展操作符来操作
const i16Array = new Int16Array([1, 2, 3]);
for (const i16value of i16Array) {
log(i16value); // 依此输出 1,2,3
}
log(Math.max(...i16Array)); // 3
set()
将 array 拷贝到 offset 起始的位置,改变调用者的值(但是不会改变定型数组的长度,超过长度会报错)
// ts set(array: ArrayLike<number>, offset?: number | undefined): void
// 创建长度为 8 的 int16 数组
const container = new Int16Array(8);
// 把定型数组复制为前 4 个值,
container.set(Int8Array.of(1, 2, 3, 4));
log(container); // Int16Array(8) [ 1, 2, 3, 4, 0, 0, 0, 0 ]
// 把普通数组复制到 container 的后 4 个值, 需要设置偏移量为 4
container.set([5, 6, 7, 8], 4);
log(container); // Int16Array(8) [1,2,3,4,5,6,7,8];
// 溢出会抛出错误
log(container.length);
container.set([8, 9, 10], 7); // RangeError: offset is out of bounds, 因为 container.length 等于 8, 而要从索引 7 在复制 3 个数, 7+3 > 8 溢出了
##### subarray
subarray 会基于从原始定型数组中复制的值返回一个定型数组,复制值时的开始索引和结束索引时可选的
// ts 声明:subarray: <T>(begin?: number, end?: number) => T
const source = Int16Array.of(2, 4, 6, 8);
// 把整个数组复制为同一个类型的新数组
const fullCopy = source.subarray();
log(fullCopy); // log: Int16Array(4) [ 2, 4, 6, 8 ]
const sub2 = source.subarray(2);
log(sub2); // log: Int16Array(2) [ 6, 8 ]
定型数组没有原生的拼接能力,但是使用定型数组提供的许多 API 可以手动构建
// v1.0
const typeArrayConcat = (TypeConstructor, ...typeArrays) => {
const len = typeArrays.reduce((len, current) => (len += current.length), 0);
const result = new TypeConstructor(len);
let idx = 0;
// 外层循环
for (let outIdx = 0; outIdx < typeArrays.length; outIdx++) {
const currentTypeArray = typeArrays[outIdx];
// 内层循环
for (let innerIdx = 0; innerIdx < currentTypeArray.length; innerIdx++) {
result[idx++] = currentTypeArray[innerIdx];
}
}
return result;
};
// v2
const typeArrayConcat2 = (TypeConstructor, ...typeArrays) => {
const len = typeArrays.reduce((len, current) => (len += current.length), 0);
const result = new TypeConstructor(len);
// 记录当前装了几个值
let currentOffSet = 0;
typeArrays.forEach((innerArr) => {
result.set(innerArr, currentOffSet);
currentOffSet += innerArr.length;
});
return result;
};
const concatArray = typeArrayConcat(
Int32Array,
Int8Array.of(1, 2, 3),
Int16Array.of(4, 5, 6),
Int32Array.of(7, 8, 9)
);
log(concatArray); // log: Int32Array(9) [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
下溢和上溢
定型数组中值的下溢和上溢不会影响到其他索引,但仍然需要考虑数组的元素应该是什么类型,定型数组对于可以储存的每个索引值接受一个相关位,不考虑他们对时机数值的影响
/**
* 长度为 2 的有符号整数数组,每个索引保存一个二补数形式的有符号整数,范围是 -128(-1*2**7)~ 127(2**7-1)
*/
const i8a1 = new Int8Array(2);
i8a1[1] = 128 // 128 的二进制位 10000000, 在 Int8Array 中最高位是符号位,0正1负,所以位 -128
log(i8a1) // Int8Array(2) [ 0, -128 ]
/**
* 长度为 2 的无符号整数数组,每个索引保持一个无符号整数,范围是 0 ~ 255(2**8-1)
*/
const u8a1 = new Uint8Array(2);
/**
* 上溢的位不会影响相邻索引,Uint8Array 索引只取最低有效位上的 8 位
*/
u8a1[1] = 256; // 这里发生了上溢,因为 uint8 最大的有效 无符号整数 位 255,256 大于了 255,导致出现上溢
log(u8a1); // log: Uint8Array(2) [ 0, 0 ]
u8a1[1] = 511; // 0x1ff(16进制) = 1 1111 1111(二进制) 是以二补数形式表示的 -1 (截取到8位后等于 1111 1111 = 2**8 -1 = 255),下溢的位会被转换为其服务号的等价值
log(u8a1); // log: Uint8Array(2) [ 0, 255 ]
###### Uint8ClampedArray 不允许任何方向溢出
Uint8ClampedArray 超过最大值 255 的时候会被向下舍入位 255,而小于 0 的值会被向上舍入位 0,这个是 HTML5canvas 元素的历史存留,除非想做跟 canvas 相关的开发,否则不要用它
const clampedInts = new Uint8ClampedArray([-1, 0, 255, 256]);
log(clampedInts); // Uint8ClampedArray(4) [ 0, 0, 255, 255 ] 看! 自动向上舍入 和向下舍入了