Skip to content

深入理解 TypedArray 与 ArrayBuffer

二进制数据操作完整API、SharedArrayBuffer、Atomics 与实际应用场景

什么是 TypedArray 与 ArrayBuffer?

定义ArrayBuffer 是一块固定长度的连续内存区域,本身不能直接读写,必须通过"视图"来操作。TypedArray(如 Uint8ArrayFloat32Array)和 DataView 就是这样的视图,它们以特定的数值类型解释 ArrayBuffer 中的二进制数据,提供高效的类型化数组操作。

涉及场景

  • WebGL / Canvas:图形渲染需要大量浮点数组(顶点、颜色、纹理坐标)
  • Web Audio API:音频采样数据以 Float32Array 表示
  • WebSocket 二进制通信:发送/接收二进制帧(如 Protobuf、MessagePack)
  • File API / Blob:文件读取、切片上传、图片处理
  • WASM 交互:WebAssembly 模块与 JS 之间通过 ArrayBuffer 传递数据
  • 多线程共享SharedArrayBuffer + Atomics 实现 Web Worker 间的内存共享

作用

  1. 高性能数据处理:连续内存 + 固定类型,比普通 Array 快数十倍
  2. 精确内存控制:精确到字节级别的读写,适合处理底层协议和文件格式
  3. 跨平台互操作:与 C/C++/Rust(WASM)共享同一块内存,零拷贝传输
  4. 并发编程基础SharedArrayBuffer + Atomics 是 JS 多线程共享内存的唯一方式

为什么需要二进制操作?

JavaScript 最初设计用于操作文本和DOM,但随着 WebGL、WebSocket、File API、Web Audio 等技术的发展,需要高效操作二进制数据

javascript
// 传统 Array 的问题:
const arr = [1, 2, 3]; // 每个元素可以是任意类型,内存不连续
// V8 内部每个元素占用 8 字节(tagged pointer),还有类型检查开销

// TypedArray 的优势:
const uint8 = new Uint8Array([1, 2, 3]); // 每个元素固定 1 字节,连续内存
// 更快的访问速度,更少的内存占用

核心概念

ArrayBuffer(原始二进制缓冲区)
  ↓ 不能直接读写,需要通过视图
  ├── TypedArray(类型化数组视图)
  │   ├── Int8Array      / Uint8Array      / Uint8ClampedArray  (1字节)
  │   ├── Int16Array     / Uint16Array                          (2字节)
  │   ├── Int32Array     / Uint32Array                          (4字节)
  │   ├── Float32Array   / Float64Array                         (4/8字节)
  │   └── BigInt64Array  / BigUint64Array                       (8字节)
  └── DataView(灵活的多类型视图)

ArrayBuffer

javascript
// 创建 16 字节的缓冲区(所有字节初始化为 0)
const buffer = new ArrayBuffer(16);

console.log(buffer.byteLength); // 16

// 不能直接读写
// buffer[0] = 1; // ❌ 无效

// 检查是否是 ArrayBuffer
buffer instanceof ArrayBuffer; // true
ArrayBuffer.isView(buffer);    // false

// 切片(创建副本)
const slice = buffer.slice(0, 8); // 前8字节的副本

// 调整大小(ES2024)
const resizable = new ArrayBuffer(8, { maxByteLength: 16 });
resizable.resize(12); // 扩展到 12 字节
resizable.resizable;  // true

// 转移所有权(ES2024)
const buf1 = new ArrayBuffer(8);
const buf2 = buf1.transfer(); // buf1 变为 detached,buf2 获得数据
// buf1.byteLength === 0(已分离)
// buf2.byteLength === 8

TypedArray(类型化数组)

所有类型

类型字节范围等价C类型
Int8Array1-128 ~ 127int8_t
Uint8Array10 ~ 255uint8_t
Uint8ClampedArray10 ~ 255(钳制)-
Int16Array2-32768 ~ 32767int16_t
Uint16Array20 ~ 65535uint16_t
Int32Array4-2³¹ ~ 2³¹-1int32_t
Uint32Array40 ~ 2³²-1uint32_t
Float32Array4±3.4e38float
Float64Array8±1.8e308double
BigInt64Array8-2⁶³ ~ 2⁶³-1int64_t
BigUint64Array80 ~ 2⁶⁴-1uint64_t

创建方式

javascript
// 1. 指定长度
const a = new Uint8Array(4); // [0, 0, 0, 0]

// 2. 从数组创建
const b = new Float32Array([1.5, 2.5, 3.5]);

// 3. 从另一个 TypedArray 创建(类型转换)
const c = new Uint8Array(new Float32Array([1.5, 2.7])); // [1, 2](截断)

// 4. 从 ArrayBuffer 创建(共享内存!)
const buffer = new ArrayBuffer(8);
const view1 = new Uint8Array(buffer);      // 8个 uint8
const view2 = new Uint16Array(buffer);     // 4个 uint16
const view3 = new Float64Array(buffer);    // 1个 float64
// 三个视图共享同一段内存!

view1[0] = 255;
console.log(view2[0]); // 255(共享内存)

// 5. 从 ArrayBuffer 的一部分创建
const partial = new Uint8Array(buffer, 2, 4);
// 从偏移量2开始,长度4

// 6. 静态方法
const d = Uint8Array.from([1, 2, 3]);
const e = Uint8Array.of(1, 2, 3);

Uint8ClampedArray 的特殊性

javascript
// 普通 Uint8Array:溢出时截断(取低8位)
const u8 = new Uint8Array([256, -1, 300]);
console.log(u8); // [0, 255, 44](256→0, -1→255, 300→44)

// Uint8ClampedArray:溢出时钳制到 0-255
const clamped = new Uint8ClampedArray([256, -1, 300]);
console.log(clamped); // [255, 0, 255](超过255→255, 小于0→0)

// Canvas 的 ImageData 就使用 Uint8ClampedArray
// 像素值自然在 0-255 范围内

TypedArray 的方法

javascript
const arr = new Float32Array([3, 1, 4, 1, 5, 9]);

// 与普通 Array 相同的方法
arr.length;          // 6
arr[0];              // 3
arr.slice(0, 3);     // Float32Array [3, 1, 4]
arr.map(x => x * 2); // Float32Array [6, 2, 8, 2, 10, 18]
arr.filter(x => x > 3); // Float32Array [4, 5, 9]
arr.reduce((a, b) => a + b); // 23
arr.find(x => x > 4); // 5
arr.indexOf(4);      // 2
arr.includes(9);     // true
arr.forEach(x => {});
arr.some(x => x > 8); // true
arr.every(x => x > 0); // true
arr.sort();          // Float32Array [1, 1, 3, 4, 5, 9]
arr.reverse();

// TypedArray 独有的方法
arr.set([10, 20], 2);  // 从索引2开始设置值
arr.subarray(1, 3);    // 返回视图(共享内存,不是副本!)

// 与 Array 的区别
// ❌ 不支持:push, pop, shift, unshift, splice, concat
// ❌ 长度固定,不能改变
// ✅ 支持:for...of、展开运算符、解构

// 属性
arr.buffer;       // 底层 ArrayBuffer
arr.byteOffset;   // 在 buffer 中的偏移量
arr.byteLength;   // 占用字节数
arr.BYTES_PER_ELEMENT; // 每个元素的字节数

DataView(灵活视图)

javascript
// DataView 可以在同一个 buffer 上混合读写不同类型
const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);

// 写入(可指定字节序)
view.setUint8(0, 255);           // 偏移0,写入 uint8
view.setInt16(1, -1000, true);   // 偏移1,写入 int16(小端序)
view.setFloat32(4, 3.14, true);  // 偏移4,写入 float32
view.setFloat64(8, Math.PI);     // 偏移8,写入 float64(大端序默认)

// 读取
view.getUint8(0);              // 255
view.getInt16(1, true);        // -1000
view.getFloat32(4, true);      // 3.140000104904175
view.getFloat64(8);            // 3.141592653589793

// DataView 的全部方法
// get/set + Int8/Uint8/Int16/Uint16/Int32/Uint32/Float32/Float64/BigInt64/BigUint64

字节序(Endianness)

javascript
// 大端序(Big-Endian):高位字节在前(网络字节序)
// 小端序(Little-Endian):低位字节在前(x86/ARM)

// 数字 0x12345678 在内存中:
// 大端序:[12] [34] [56] [78]
// 小端序:[78] [56] [34] [12]

// 检测系统字节序
function isLittleEndian() {
  const buffer = new ArrayBuffer(2);
  new DataView(buffer).setInt16(0, 256, true); // 小端序写入 256
  return new Int16Array(buffer)[0] === 256;
}
console.log(isLittleEndian()); // 大多数系统:true

// TypedArray 使用系统字节序(通常是小端)
// DataView 可以显式指定字节序(第三个参数 true = 小端)
// 处理网络数据时要注意字节序转换

实际应用场景

1. Canvas 图像处理

javascript
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = 200;
canvas.height = 200;

// 获取像素数据
const imageData = ctx.getImageData(0, 0, 200, 200);
const pixels = imageData.data; // Uint8ClampedArray [R,G,B,A, R,G,B,A, ...]

// 灰度化
for (let i = 0; i < pixels.length; i += 4) {
  const gray = pixels[i] * 0.299 + pixels[i+1] * 0.587 + pixels[i+2] * 0.114;
  pixels[i] = pixels[i+1] = pixels[i+2] = gray;
  // pixels[i+3] 是 alpha,不修改
}

ctx.putImageData(imageData, 0, 0);

// 反色
for (let i = 0; i < pixels.length; i += 4) {
  pixels[i]     = 255 - pixels[i];     // R
  pixels[i + 1] = 255 - pixels[i + 1]; // G
  pixels[i + 2] = 255 - pixels[i + 2]; // B
}

2. 文件读取与解析

javascript
// 读取文件为 ArrayBuffer
const input = document.querySelector('input[type="file"]');
input.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const buffer = await file.arrayBuffer();

  // 解析 PNG 文件头
  const header = new Uint8Array(buffer, 0, 8);
  const isPNG = header[0] === 0x89 &&
                header[1] === 0x50 && // P
                header[2] === 0x4E && // N
                header[3] === 0x47;   // G
  console.log('Is PNG:', isPNG);

  // 解析 BMP 文件头
  const view = new DataView(buffer);
  if (view.getUint16(0, true) === 0x4D42) { // 'BM'
    const fileSize = view.getUint32(2, true);
    const width = view.getInt32(18, true);
    const height = view.getInt32(22, true);
    console.log(`BMP: ${width}x${height}, ${fileSize} bytes`);
  }
});

3. WebSocket 二进制通信

javascript
const ws = new WebSocket('wss://example.com');
ws.binaryType = 'arraybuffer';

// 发送二进制数据
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setUint8(0, 0x01);      // 消息类型
view.setUint16(1, 1024);     // 数据长度
view.setFloat32(3, 3.14);    // 数据
ws.send(buffer);

// 接收二进制数据
ws.onmessage = (event) => {
  if (event.data instanceof ArrayBuffer) {
    const view = new DataView(event.data);
    const type = view.getUint8(0);
    const length = view.getUint16(1);
    console.log(`Type: ${type}, Length: ${length}`);
  }
};

4. Base64 编解码

javascript
// ArrayBuffer → Base64
function bufferToBase64(buffer) {
  const bytes = new Uint8Array(buffer);
  let binary = '';
  for (const byte of bytes) {
    binary += String.fromCharCode(byte);
  }
  return btoa(binary);
}

// Base64 → ArrayBuffer
function base64ToBuffer(base64) {
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes.buffer;
}

// 字符串 → ArrayBuffer(UTF-8)
const encoder = new TextEncoder();
const encoded = encoder.encode('Hello 你好'); // Uint8Array

// ArrayBuffer → 字符串
const decoder = new TextDecoder('utf-8');
const decoded = decoder.decode(encoded); // 'Hello 你好'

SharedArrayBuffer 与 Atomics

SharedArrayBuffer

javascript
// SharedArrayBuffer 可以在多个 Worker 之间共享内存
const sharedBuffer = new SharedArrayBuffer(1024);

// 主线程
const worker = new Worker('worker.js');
worker.postMessage(sharedBuffer);

// worker.js
onmessage = (e) => {
  const shared = new Int32Array(e.data);
  shared[0] = 42; // 修改共享内存,主线程也能看到
};

// ⚠️ 安全要求(Spectre 漏洞后)
// 需要设置 HTTP 头:
// Cross-Origin-Opener-Policy: same-origin
// Cross-Origin-Embedder-Policy: require-corp

Atomics(原子操作)

javascript
// 多线程访问共享内存时,需要原子操作避免竞态条件
const shared = new Int32Array(new SharedArrayBuffer(4));

// 原子读写
Atomics.store(shared, 0, 42);    // 原子写入
Atomics.load(shared, 0);         // 原子读取 → 42

// 原子算术
Atomics.add(shared, 0, 10);      // 原子加法,返回旧值 42
Atomics.sub(shared, 0, 5);       // 原子减法
Atomics.and(shared, 0, 0xFF);    // 原子按位与
Atomics.or(shared, 0, 0x0F);     // 原子按位或
Atomics.xor(shared, 0, 0xFF);    // 原子按位异或
Atomics.exchange(shared, 0, 100); // 原子交换,返回旧值
Atomics.compareExchange(shared, 0, 100, 200);
// 如果 shared[0] === 100,则设为 200,返回旧值

// 等待/唤醒(线程同步)
// Worker 中:
Atomics.wait(shared, 0, 0); // 等待 shared[0] 不等于 0(阻塞)

// 主线程:
Atomics.store(shared, 0, 1);
Atomics.notify(shared, 0, 1); // 唤醒一个等待的线程

总结

TypedArray & ArrayBuffer 核心知识点:
┌──────────────────────────────────────────────────────────┐
│ 核心概念                                                  │
│ • ArrayBuffer:原始二进制缓冲区(不能直接读写)               │
│ • TypedArray:固定类型的视图(11种类型)                     │
│ • DataView:灵活的多类型视图(可指定字节序)                  │
├──────────────────────────────────────────────────────────┤
│ 应用场景                                                  │
│ • Canvas 图像处理(ImageData.data)                        │
│ • 文件读取与二进制协议解析                                   │
│ • WebSocket / WebRTC 二进制通信                            │
│ • WebGL 顶点数据                                          │
│ • Web Audio 音频数据                                      │
│ • Base64 / TextEncoder/TextDecoder                       │
├──────────────────────────────────────────────────────────┤
│ 多线程                                                    │
│ • SharedArrayBuffer:跨 Worker 共享内存                    │
│ • Atomics:原子操作,避免竞态条件                            │
│ • 需要 COOP/COEP 安全头                                   │
└──────────────────────────────────────────────────────────┘