ES6 TypedArray

最后更新:
阅读次数:

起源

随着 Web 应用程序变得越来越强大,尤其是一些新增加的功能(例如:音频视频编辑,WebGL 绘图,访问 WebSockets 的原始二进制数据等),都需要用 JavaScript 直接操作二进制数据。但是在 ES6 之前,JavaScript 并没有相关的接口去直接操作二进制数据。所以为了解决这个问题,ES6 中引入了 ArrayBufferDataViewTypedArray 接口。

  • ArrayBuffer:创建一段二进制数据缓冲区
  • DataView:创建一个通用的视图
  • TypedArray:创建一个有固定数值数据类型的视图

一个完整的操作二进制数据的流程是这样的:先创建一个数据缓冲区,再创建一个视图来操作数据缓冲区中的二进制数据。

数值数据类型

JavaScript 中的数字是按照 IEEE 754 标准定义的格式存储的,其数值数据类型是 64 位浮点数 (float64),即在内存中是用 64 个比特(即 8 个字节)来存储的。

  • 目前,共有 8 种数值数据类型
有符号的 8 位整数(int8)
无符号的 8 位整数(uint8)
有符号的 16 位整数(int16)
无符号的 16 位整数(uint16)
有符号的 32 位整数(int32)
无符号的 32 位整数(uint32)
32 位浮点数(float32)
64 位浮点数(float64)

如果用普通的 JavaScript 数字来存储一个 8 位整数(比如,123),那么会浪费整整 56 个比特。而定型数组的出现,正是为了更有效地利用这些被浪费的比特。

数组缓冲区

数组缓冲区是所有定型数组的根基,它是一段可以包含特定数量字节的内存地址。

比特:bit(别称:位)
字节:byte
千字节:KB(Kilobyte)
1 Byte = 8 Bits
1 KB = 1024 Bytes

ArrayBuffer 类型

通过 new ArrayBuffer(num) 可以创建一段通用的、有固定长度的原始二进制数据缓冲区。

但是我们不能直接操作数据缓存区(即不能直接对数据缓存区中的数据进行读写操作),而是要通过创建一个视图(比如:定型数组对象 或 DataView 对象)来操作数组缓冲区。

// 创建一个 10 字节长度的数组缓冲区
// 单位:字节。 10字节 = 80比特
let buffer = new ArrayBuffer(10);

console.log(buffer.byteLength);

// 通过使用 slice() 方法基于已有的数组缓冲区来创建新的数组缓冲区
let buffer2 = buffer.slice(2, 5);

视图

数组缓冲区是内存中的一段地址,而视图是用来操作内存的接口。视图可以操作数组缓冲区,并按照某种数值型数据类型来读取和写入数据。

DataView 类型的视图

通过 new DataView(buffer) 可以创建一种通用的数组缓冲区视图。该视图支持上面提到的 8 种数值数据类型,并且为每一种数值类型都定义了对应的操作函数。

比如:对于 有符号的 8 位整数 类型,DataView 类型的视图提供了 DataView.prototype.getInt8(byteOffset)DataView.prototype.setInt8(byteOffset, value) 方法。

let buffer = new ArrayBuffer(10);
let view = new DataView(buffer);

view.setInt8(0, 100);
view.setInt8(1, 127);
view.setInt8(2, 128);

console.log(view.getInt8(0)); // 100
console.log(view.getInt8(1)); // 127
console.log(view.getInt8(2)); // -128 这里是因为有符号 8 位整数的最大值为 127
// 以 16 位整数类型读取两个 8 位整数类型的值
let buffer = new ArrayBuffer(2);
let view = new DataView(buffer);

view.setInt8(0, 4);
view.setInt8(1, 5);

console.log(view.getInt8(0)); // 4
console.log(view.getInt8(1)); // 5
console.log(view.getInt16(0)); // 1029

当混合使用不同数据类型时,DataView 对象是一个完美的选择。然而,如果你只使用某种特定的数值数据类型,那么有特定数值类型的视图则是更好的选择。

TypedArray 类型的视图

  • ES6 定义的定型数组实际上就是用于数据缓冲区的特性类型的视图

  • 目前,TypedArray 类型的视图共包括下面的 9 种类型

类型范围占用字节数描述Web IDL typeEquivalent C type
Int8Array-128 至 1271有符号的 8 位整数byteint8_t
Uint8Array0 至 2551无符号的 8 位整数octetuint8_t
Uint8ClampedArray0 至 2551无符号的 8 位整数(自动过滤溢出)octetuint8_t
Int16Array-32768 至 327672有符号的 16 位整数shortint16_t
Uint16Array0 至 655352无符号的 16 位整数unsigned shortuint16_t
Int32Array-2147483648 至 21474836474有符号的 32 位整数longint32_t
Uint32Array0 至 42949672954无符号的 32 位整数unsigned longuint32_t
Float32Array1.2x10 -38 至 3.4x10 38432 位浮点数unrestricted floatfloat
Float64Array5.0x10 -324 至 1.8x10 308864 位浮点数unrestricted doubledouble
// 1. 基于 ArrayBuffer 来创建特定视图
let buffer = new ArrayBuffer(3);
let view = new Int8Array(buffer);

view[0] = 122;
view[1] = 125;
view[2] = 129; // 越界,最大值应该为 127

console.log(view); // Int8Array(3) [122, 125, -127]
// 2. 直接指定视图元素数量来创建特定视图
let view = new Int8Array(4);

console.log(view); // Int8Array(4) [0, 0, 0, 0]

view[2] = 66;

console.log(view); // Int8Array(4) [0, 0, 66, 0]
// 3. 直接给构造函数传入一个定型数组或一个可迭代对象或一个数组或一个类数组对象来创建特定视图
let arr = [1, 2, 33];
let view = new Uint8Array(arr);

console.log(view); // Uint8Array(3) [1, 2, 33]
  • 不同的视图类型,所能容纳的数值范围是确定的。超出这个范围,就会出现 位溢出现象

TypedArray 处理位溢出的规则很简单,就是抛弃溢出的位,只取符合当前数值类型的位数上的数据

  • 可以总结为一个简单转换规则,如下:
    • 正向溢出(overflow): 当输入值大于当前数据类型的最大值,结果等于当前数据类型的最小值加上余值,再减去 1
    • 负向溢出(underflow): 当输入值小于当前数据类型的最小值,结果等于当前数据类型的最大值减去余值,再加上 1
const uint8 = new Uint8Array(1);

// 十进制:255
// 二进制:1111 1111
uint8[0] = 255;
console.log(uint8[0]); // 255

// 十进制:256
// 二进制:1000 0000 0
uint8[0] = 256;
console.log(uint8[0]); // 0

// 十进制:-1
// 二进制:1111 1111 1111 1111 1111 1111 1111 1111
uint8[0] = -1;
console.log(uint8[0]); // 255

因为上面使用的是 Uint8 类型,所以对于 256-1 的二进制只取前 8 位,所以才有了后面奇怪的输出。

  • Uint8ClampedArray 类型的视图的溢出规则,与上面的规则不同

它规定,凡是发生正向溢出,该值一律等于当前数据类型的最大值,即 255;如果发生负向溢出,该值一律等于当前数据类型的最小值,即 0。

const uint8c = new Uint8ClampedArray(1);

uint8c[0] = 255;
console.log(uint8c[0]); // 255

uint8c[0] = 256;
console.log(uint8c[0]); // 255

uint8c[0] = -1;
console.log(uint8c[0]); // 0
  • 复合视图

由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同数值类型的数据,这叫做 复合视图。

这里的复合视图和上面的 DataView 视图 倒是有点类似,都可以存放多种数值类型的数据。

// 定义一个有 24 个字节的 buffer
const buffer = new ArrayBuffer(24);

// 定义一个 32 位无符号整数,占用 4 个字节
const unit32 = new Uint32Array(buffer, 0, 1);

// 定义十六个 8 位无符号整数,占用 16 个字节
const unit8 = new Uint8Array(buffer, 4, 16);

// 定义一个 32 位浮点数,占用 4 个字节
const float32 = new Float32Array(buffer, 20, 1);

定型数组 vs 普通数组

  • 定型数组不是普通数组,因为它不继承自 Array 对象,它只是一种用于处理数值类型数据的类数组对象

定型数组(即 TypedArray)只是对所有特定视图类型的统称,目前并不存在实际的 TypedArray 构造函数,或者 TypedArray 类型。

let typedArr = new Int8Array(5);

console.log(typedArr instanceof Array); // false
console.log(Array.isArray(typedArr)); // false

console.log(typedArr instanceof TypedArray); // Uncaught ReferenceError: TypedArray is not defined

// 在 Chrome 上,出现了下面奇怪的现象
console.log(Object.getPrototypeOf(typedArr));
// 打印 TypedArray {constructor: ƒ, BYTES_PER_ELEMENT: 1}
// 个人感觉是 JavaScript 内建的对象并没有实现 TypedArray 类型,但是 Chrome 内部实现了,并且没直接暴露给开发者
  • 普通数组的大小可以随时动态调整,而定型数组不能
let arr = [1, 2];
let typedArr = new Int8Array(arr);

console.log(arr); // [1, 2]
console.log(typedArr); // Int8Array(2) [1, 2]

arr[4] = 66;
typedArr[4] = 66;

console.log(arr); // [1, 2, undefined, undefined, 66]
console.log(typedArr); // Int8Array(2) [1, 2]
  • 与普通数组相比,定型数组没有以下的方法
// 由于定型数组的尺寸不可更改,所以没有下面的方法
concat();
shift();
pop();
splice();
push();
unshift();
  • 与普通数组相比,定型数组有下面两个额外的方法
// set() 将传入的普通数组或定型数组按指定的偏移量插入定型数组中
let typedArr = new Int8Array(4);

typedArr.set([25, 50]);
typedArr.set([11, 22], 2);

console.log(typedArr);
// subarray()
// returns a new TypedArray on the same ArrayBuffer store and
// with the same element types as for this TypedArray object
let typedArr = new Int8Array([1, 2, 33, 44]);
let subArr = typedArr.subarray(2);

console.log(subArr); // Int8Array(2) [33, 44]
console.log(typedArr.buffer === subArr.buffer); // true

typedArr[2] = 66;

console.log(typedArr); // Int8Array(4) [1, 2, 66, 44]
console.log(subArr); // Int8Array(2) [66, 44]
// 区别 slice() 与 subarray()
let typedArr = new Int8Array([1, 2, 33, 44]);
let sliceArr = typedArr.slice(2);

console.log(sliceArr); // Int8Array(2) [33, 44]
console.log(typedArr.buffer === sliceArr.buffer); // false

typedArr[2] = 66;

console.log(typedArr); // Int8Array(4) [1, 2, 66, 44]
console.log(sliceArr); // Int8Array(2) [33, 44]