前言
- 在 JavaScript 中,TypedArray 是一种特殊的数组类型,它提供了一种用于在内存缓冲中访问原始二进制数据的机制。与普通的 JavaScript 数组不同,TypedArray 可以存储多种数据类型,如整数、浮点数和布尔值等。
- TypedArray 对象描述了底层二进制数据缓冲区的类数组视图。没有称为 TypedArray 的全局属性,也没有直接可用的 TypedArray 构造函数。但是,有很多不同的全局属性,其值是指定元素类型的类型化数组构造函数。
1)家族成员 & 核心概念
-
ArrayBuffer:原始内存块(字节数组),不含读写方法。
-
视图(View) :基于同一 ArrayBuffer 的不同"读写方式":
- TypedArray 视图(按固定类型顺序读写)
Int8Array / Uint8Array / Uint8ClampedArray
Int16Array / Uint16Array
Int32Array / Uint32Array
BigInt64Array / BigUint64Array
Float32Array / Float64Array
- DataView(按字节偏移随取随用,支持大小端)
- TypedArray 视图(按固定类型顺序读写)
你可以在同一个 ArrayBuffer 上创建多个视图(甚至重叠),实现零拷贝地读取不同字段。
javascript
const buf = new ArrayBuffer(16);
const u8 = new Uint8Array(buf); // 按字节
const f64 = new Float64Array(buf, 8, 1); // 从第8字节起读一个 Float64
2)什么时候选 TypedArray,什么时候用 DataView?
- 已知数据全是同一类型的连续块 (像像素、PCM、顶点数据):用 TypedArray,高效、简单。
- 需要按"结构体"字段解析 (混合类型、含大小端控制):用 DataView。
ini
// DataView 解析一个小端头部:magic(4B) + version(Uint16) + flags(Uint8)
function parseHeader(buf) {
const dv = new DataView(buf);
const magic = dv.getUint32(0, true); // true = little-endian
const version = dv.getUint16(4, true);
const flags = dv.getUint8(6);
return { magic, version, flags };
}
3)构造/切片/拷贝的必会用法
javascript
const u8 = new Uint8Array(4); // 4 个字节,默认 0
u8.set([1,2,3], 1); // 从索引 1 开始写入
const part = u8.subarray(1, 3); // 视图(零拷贝)
const copy = u8.slice(1, 3); // 拷贝出新 Buffer(有拷贝)
// typed ↔ normal array
const arr = Array.from(u8); // [0,1,2,3]
const u16 = new Uint16Array([500, 1000]); // 溢出会按类型截断/取模
// 注意:Uint8ClampedArray 会将值夹在 [0, 255]
new Uint8ClampedArray([-20, 260]); // -> [0, 255]
口诀:subarray 不拷贝、slice 会拷贝。大文件/流式处理优先 subarray。
4)文本编码/解码(UTF-8 安全方案)
不要用 atob/btoa
搞 UTF-8 文本,它们是 Latin-1。请用标准 API:
ini
const enc = new TextEncoder(); // UTF-8
const dec = new TextDecoder('utf-8');
const u8 = enc.encode('你好 👋'); // Uint8Array
const str = dec.decode(u8); // '你好 👋'
5)与 Web API 的高频搭配
Fetch 流式下载
ini
const res = await fetch(url);
const reader = res.body.getReader(); // chunks: Uint8Array
let received = 0;
for (;;) {
const { value, done } = await reader.read();
if (done) break;
received += value.byteLength; // 直接处理二进制块
}
Blob / File / 下载
ini
const u8 = new Uint8Array([0xFF, 0xD8, /* ... */]); // JPEG 片段
const blob = new Blob([u8], { type: 'image/jpeg' });
const url = URL.createObjectURL(blob);
Canvas / WebGL / WebAudio / WASM
- Canvas:
ctx.putImageData()
/ImageData.data
是Uint8ClampedArray
- WebGL:
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW)
- WebAudio:
AudioBuffer.getChannelData()
→Float32Array
- WASM:与模块共享其 Linear Memory (ArrayBuffer) ,在 JS 用
new Uint8Array(wasmMemory.buffer)
读写
6)与 Node.js:Buffer 互操作
在 Node 里,Buffer
是 Uint8Array
的子类,可直接互转:
ini
const buf = Buffer.from([1,2,3]); // Node Buffer
const u8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
大多数库都接受
Uint8Array
;在浏览器用 TypedArray,在 Node 用 Buffer/Uint8Array 都 OK。
7)跨线程传输与共享(性能关键)
结构化克隆 & 可转移(postMessage)
scss
// 主线程 → Worker
const ab = new ArrayBuffer(1024);
postMessage(ab, [ab]); // 转移所有权:零拷贝
// 注意:转移后 ab.byteLength 变为 0(已移交)
共享内存(SharedArrayBuffer + Atomics)
适合生产者/消费者模型、无锁并发计数等:
javascript
// 共享计数器
const sab = new SharedArrayBuffer(4);
const i32 = new Int32Array(sab);
Atomics.add(i32, 0, 1); // 线程安全 + 可唤醒
const v = Atomics.load(i32, 0);
开启 SAB 需站点隔离头(COOP/COEP)等安全前置,生产环境再用。
8)大小端(Endianness)不会?就记这条
- TypedArray 读写使用宿主平台的端序,但你几乎感知不到(因为它以"元素"为单位)。
- DataView 的
getUint32(offset, littleEndian)
必须显式传端序 来匹配协议规范。多数网络/文件协议是小端(比如 protobuf、WAV 等常见格式),也有大端的(例如某些老协议)。
9)性能与内存小贴士
- 避免不必要的拷贝 :优先使用
subarray
/多视图共享一个ArrayBuffer
。 - 批量写入 :
typed.set(otherTyped, offset)
一次性写,比循环快。 - 复用缓冲区:频繁分配大 ArrayBuffer 会触发 GC;复用池更稳。
- 边界与对齐 :
Int16Array
/Float32Array
等建议在对齐地址上读写(大多数 JS 引擎已优化,但跨平台解析二进制文件时自己保证结构体布局更稳)。 - BigInt 系列 :
BigInt64Array
/BigUint64Array
只能与BigInt
值互操作,不能与普通number
混用。
10)典型实战片段
A. 解析"结构体"头 + 后续 payload
ini
function parsePacket(buf) {
const dv = new DataView(buf);
const len = dv.getUint32(0, true); // payload 长度 (LE)
const type = dv.getUint16(4, true);
const flags = dv.getUint8(6);
const payload = new Uint8Array(buf, 7, len); // 零拷贝视图
return { len, type, flags, payload };
}
B. 拼接多个分片(零拷贝合并并不现实,需新缓冲)
csharp
function concat(chunks) {
const total = chunks.reduce((s, c) => s + c.byteLength, 0);
const out = new Uint8Array(total);
let off = 0;
for (const c of chunks) { out.set(c, off); off += c.byteLength; }
return out;
}
C. Base64 ↔ Uint8Array(浏览器现代安全做法)
javascript
async function base64ToBytes(b64) {
const res = await fetch(`data:application/octet-stream;base64,${b64}`);
return new Uint8Array(await res.arrayBuffer());
}
function bytesToBase64(u8) {
return btoa(String.fromCharCode(...u8)); // 小数据适用;大数据用分块
}
11)常见坑
- 把普通 Array 当成二进制容器 :会慢且占内存;用
Uint8Array
。 - 误用 atob/btoa 处理 UTF-8 :请用
TextEncoder/Decoder
。 - 忘了端序 :
DataView
一律明确littleEndian
参数。 - 滥用 slice 拷贝 :先问自己能否
subarray
。 - bigint 与 number 混用:在 BigInt TypedArray 中只能用 BigInt 值。
- 跨线程大量拷贝 :使用 可转移对象(第二参数)或 SAB。
结语
记住三件事:ArrayBuffer 是底座,TypedArray 读写同类数据,DataView 解析结构体与端序。把上面的模式背下来,你在前端处理任何二进制任务都会顺手很多。需要我把你项目里的某份二进制协议/文件格式(例如某设备上报、WAV/PNG/自定义包)用 TypedArray+DataView 写成解析器模板吗?