类型化数组

Oct 31, 2024
18 min read
4153

学习 webgl/three.js 之前(主要是完成毕设),准备去 MDN 学习一下类型化数组。

类型化数组是一种类似数组的对象,但是跟我们常说的“伪数组”不同,它的主要作用是提供了一种在内存缓存中访问二进制数据的机制。JavaScript 引入它主要是为了操作一些音视频以及 webgl 的原始数据。

类型化数组与普通数组类似,但是类型化数组并不包括在数组里,体现在类型化数组调用 Array.isArray 会返回 false,另外,类型化数组不支持 pushpop 等方法。

JavaScript 将类型化数组拆分为缓冲视图两部分:

  • 缓冲:表示数据块的对象,没有格式可言,也不提供访问其内容的机制
  • 视图:缓冲不提供访问机制,所以需要用到视图,视图提供了上下文,即数据类型、起始偏移量和元素数量

缓冲

缓冲有两种类型:ArrayBufferSharedArrayBuffer,它们都是内存块的低级表示。

缓冲支持的操作:

  • 分配:创建缓冲会分配一个新的内存块,并初始化为 0
  • 复制:使用 slice 方法,可以高效地复制缓冲区的一部分数据
  • 转移(Baseline 2024):使用 transfertransferToFixedLength 方法,可以将内存块的所有权转移到一个新的缓冲对象,可以在不同执行上下文间转换数据。但是 SharedArrayBuffer 不能被转移,因为已被所有执行上下文共享
  • 调整大小(Baseline 2024)::使用 resize 方法,可以调整内存块的大小(不能超过 maxByteLength限制),大小只能增长,不能缩小

另:ArrayBufferSharedArrayBuffer 之间的区别:

ArrayBuffer 同一时刻只能属于单个执行上下文,如果将 ArrayBuffer 传递给另一个执行上下文,那么它将会被转移,原本的 ArrayBuffer 将不可用。而 SharedArrayBuffer 传递给另一个上下文不会被转移,因此可以被多个执行上下文同时访问,多线程访问避免竞争,可以使用 Atomics 方法。

视图

目前有两种视图:类型化数组视图DataView。类型化数组提供了实用方法,而 DataView 提供的操作更底层。

两种视图都会使 ArrayBuffer.isView 方法返回 true,都具备以下属性:

  • buffer:视图所引用的低层缓冲
  • byteOffset:视图相对于换成冲起始位置偏移量(以字节为单位)
  • byteLength:视图长度(以字节为单位)

两者的构造函数还接受 length 作为数量,而不是字节长度,两种试图都会使 ArrayBuffer.isView,都具备以下属性:

  • buffer:视图所引用的底层缓冲
  • byteOffset:视图相对于缓冲起始位置的偏移量(单位字节)
  • byteLength:使徒的长度(单位字节)

类型化数组视图

除了常见的 Int8Uint32 等,还有一种特殊的类型化试图,如 Uint8ClampedArray,它会将值钳制(clamp)到 0 到 255 之间,这在 Canvas 数据处理等场景中很有用。

值范围字节大小描述Web IDL 类型等价的 C 类型
Int8Array-128 到 12718 位有符号整型(补码)byteint8_t
Uint8Array0 到 25518 位无符号整型octetuint8_t
Uint8ClampedArray0 到 25518 位无符号整型(一定在 0 到 255 之间)octetuint8_t
Int16Array-32768 到 32767216 位有符号整型(补码)shortint16_t
Uint16Array0 到 65535216 位无符号整型unsigned shortuint16_t
Int32Array-2147483648 到 2147483647432 位有符号整型(补码)longint32_t
Uint32Array0 到 4294967295432 位无符号整型unsigned longuint32_t
Float32Array-3.4E383.4E38 并且 1.2E-38 是最小的正数432 位 IEEE 浮点数(7 位有效数字,例如 1.234567unrestricted floatfloat
Float64Array-1.8E3081.8E308 并且 5E-324 是最小的正数864 位 IEEE 浮点数(16 位有效数字,例如 1.23456789012345unrestricted doubledouble
BigInt64Array-2^63 到 2^63 - 1864 位有符号整型(补码)bigintint64_t (signed long long)
BigUint64Array0 到 2^64 - 1864 位无符号整型bigintuint64_t (unsigned long long)

类型化数组原则上时固定长度的,因此不存在改变数组长度的方法,例如 poppushshiftunshiftsplice

DataView

DataVier 是一种底层接口,提供可以操作缓冲中任意数据的 getter/setter API,例如使用 DataView 获取任意数字的二进制表示:

function toBinary(x, options = {}) {
  const {
    type = 'Float64',
    littleEndian = false,
    separator = ' ',
    radix = 16,
  } = options

  const bytesNeeded = globalThis[`${type}Array`].BYTES_PER_ELEMENT
  const dv = new DataView(new ArrayBuffer(byteNeeded))

  dv[`set${type}`](0, x, littleEndian)

  const bytes = Array.from({ length: bytesNeeded }, (_, i) =>
    dv
      .getUnit8(i)
      .toString(radix)
      .padStart(8 / Math.log2(radix), '0'),
  )

  return bytes.join(separator)
}

示例

一段简单的示例:

Code Runner
// 创建“缓冲”
const buffer = new ArrayBuffer(16)
// 打印字节长度
console.log(buffer.byteLength) // 16
// 创建“视图”
const int32View = new Int32Array(buffer)
// 操作试图
for (let i = 0; i < int32View.length; i++) {
  int32View[i] = i * 2
}
// 创建另一个视图
const int16View = new Int16Array(buffer)
// 打印一下
for (let i = 0; i < int16View.length; i++) {
  console.log(`索引 ${i}${int16View[i]}`)
}
Running...

一张“图”解释:

Int16Array  |   0  |  0   |   2  |  0   |   4  |  0   |   6  |  0   |
Int32Array  |      0      |      2      |      4      |      6      |
ArrayBuffer | 00 00 00 00 | 02 00 00 00 | 04 00 00 00 | 06 00 00 00 |

缓冲中读取文本

缓冲不总是代表数字,可以使用文本缓冲读取 UTF-8 文本:

Code Runner
const buffer = new ArrayBuffer(8)
const uint8View = new Uint8Array(buffer)
// 手动写入数据,模拟一下读文件操作
uint8View.set([228, 189, 160, 229, 165, 189])
const text = new TextDecoder().decode(uint8View)
console.log(text) // 你好
Running...

读取 UTF-16 文本可以使用 String.fromCharCode() 方法:

Code Runner
const buffer = new ArrayBuffer(8)
const uint16View = new Uint16Array(buffer)
// 手动写入数据,模拟一下读文件操作
uint16View.set([0x4f60, 0x597d])
const text = String.fromCharCode(...uint16View)
console.log(text) // "你好"
Running...

复杂的数据结构

通过修改内存访问的的偏移量,可以操作很多复杂数据结构的数据,例如 C 语言结构体:

struct someStruct {
  unsigned long id;
  char username[16];
  float amountDue;
};

在 js 中可以如下代码访问一个包含结构体的缓冲:

const buffer = new ArrayBuffer(24)

// ......数据写入缓冲......

// new 视图构造函数(Buffer, ByteOffset, length)
const idView = new Uint32Array(buffer, 0, 1)
const usernameView = new Uint8Array(buffer, 4, 16)
const amountDueView = new Float32Array(buffer, 20, 1)

转化为普通数组

使用 [Array.from] 方法实现数组转换:

const typedArray = new Uint8Array([1, 2, 3, 4])
const normalArray = Array.from(typedArray)
// 或者展开语法

[...typedArray]
CC BY-NC-SA 4.0 2024 © Plumbiu