深入理解 TypedArray 与 ArrayBuffer
二进制数据操作完整API、SharedArrayBuffer、Atomics 与实际应用场景
什么是 TypedArray 与 ArrayBuffer?
定义:ArrayBuffer 是一块固定长度的连续内存区域,本身不能直接读写,必须通过"视图"来操作。TypedArray(如 Uint8Array、Float32Array)和 DataView 就是这样的视图,它们以特定的数值类型解释 ArrayBuffer 中的二进制数据,提供高效的类型化数组操作。
涉及场景:
- WebGL / Canvas:图形渲染需要大量浮点数组(顶点、颜色、纹理坐标)
- Web Audio API:音频采样数据以
Float32Array表示 - WebSocket 二进制通信:发送/接收二进制帧(如 Protobuf、MessagePack)
- File API / Blob:文件读取、切片上传、图片处理
- WASM 交互:WebAssembly 模块与 JS 之间通过
ArrayBuffer传递数据 - 多线程共享:
SharedArrayBuffer+Atomics实现 Web Worker 间的内存共享
作用:
- 高性能数据处理:连续内存 + 固定类型,比普通 Array 快数十倍
- 精确内存控制:精确到字节级别的读写,适合处理底层协议和文件格式
- 跨平台互操作:与 C/C++/Rust(WASM)共享同一块内存,零拷贝传输
- 并发编程基础:
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 === 8TypedArray(类型化数组)
所有类型
| 类型 | 字节 | 范围 | 等价C类型 |
|---|---|---|---|
Int8Array | 1 | -128 ~ 127 | int8_t |
Uint8Array | 1 | 0 ~ 255 | uint8_t |
Uint8ClampedArray | 1 | 0 ~ 255(钳制) | - |
Int16Array | 2 | -32768 ~ 32767 | int16_t |
Uint16Array | 2 | 0 ~ 65535 | uint16_t |
Int32Array | 4 | -2³¹ ~ 2³¹-1 | int32_t |
Uint32Array | 4 | 0 ~ 2³²-1 | uint32_t |
Float32Array | 4 | ±3.4e38 | float |
Float64Array | 8 | ±1.8e308 | double |
BigInt64Array | 8 | -2⁶³ ~ 2⁶³-1 | int64_t |
BigUint64Array | 8 | 0 ~ 2⁶⁴-1 | uint64_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-corpAtomics(原子操作)
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 安全头 │
└──────────────────────────────────────────────────────────┘