JavaScript TypedArray与二进制操作—ArrayBuffer、DataView与字节序控制|新宇宙博客log
byteLength
console
log
instanceof
ArrayBuffer
const
new
Uint8Array
0
255
console
log
0
内存布局示意
ArrayBuffer (16 bytes)
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │ 00 │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘
byte 0 byte 15
ArrayBuffer 转移(Transfer)
ES2024 新增 ArrayBuffer.prototype.transfer(),实现零拷贝所有权转移:
const buf = new ArrayBuffer(8);
const view = new Uint8Array(buf);
view[0] = 42;
const newBuf = buf.transfer();
console.log(buf.byteLength);
console.log(newBuf.byteLength);
console.log(new Uint8Array(newBuf)[0]);
TypedArray 家族
完整类型列表
| 类型 | 字节数 | 范围 | 用途 |
|---|
Int8Array | 1 | -128 ~ 127 | 有符号字节 |
Uint8Array | 1 | 0 ~ 255 | 无符号字节(最常用) |
Uint8ClampedArray | 1 | 0 ~ 255(夹紧) | Canvas 像素数据 |
Int16Array | 2 | -32768 ~ 32767 | 音频 PCM |
Uint16Array | 2 | 0 ~ 65535 | |
Int32Array | 4 | -2^31 ~ 2^31-1 | |
Uint32Array | 4 | 0 ~ 2^32-1 | |
Float32Array | 4 | IEEE 754 单精度 | WebGL、WebAudio |
Float64Array | 8 | IEEE 754 双精度 | 高精度运算 |
BigInt64Array | 8 | BigInt | |
BigUint64Array | 8 | BigInt | |
多视图共享同一块内存
const buf = new ArrayBuffer(4);
const u8 = new Uint8Array(buf);
const u16 = new Uint16Array(buf);
const u32 = new Uint32Array(buf);
u8[0] = 0x01;
u8[1] = 0x02;
u8[2] = 0x03;
u8[3] = 0x04;
console.log(u16[0].toString(16));
console.log(u16[1].toString(16));
console.log(u32[0].toString(16));
TypedArray 的创建方式
const buf = new ArrayBuffer(16);
const view1 = new Float32Array(buf, 4, 2);
const view2 = new Uint8Array(8);
const view3 = new Int16Array([1, 2, 3, 4]);
console.log(view3.buffer.byteLength);
const view4 = new Float64Array(view3);
const view5 = Uint8Array.from([255, 128, 0]);
const view6 = Float32Array.of(1.5, 2.5, 3.5);
常用操作
const arr = new Uint8Array([10, 20, 30, 40, 50]);
console.log(arr.length);
console.log([...arr]);
const sliced = arr.slice(1, 3);
console.log(sliced);
const sub = arr.subarray(1, 3);
sub[0] = 99;
console.log(arr[1]);
const dest = new Uint8Array(5);
dest.set([1, 2, 3], 2);
console.log(dest);
const a = new Uint8Array([1,2,3,4,5]);
a.copyWithin(0, 3);
console.log(a);
DataView 与字节序控制
什么是字节序(Endianness)?
数值 0x12345678
大端(Big-Endian):高字节在低地址
┌────┬────┬────┬────┐
│ 12 │ 34 │ 56 │ 78 │
└────┴────┴────┴────┘
addr+0 +1 +2 +3
小端(Little-Endian):低字节在低地址(x86/x64 架构)
┌────┬────┬────┬────┐
│ 78 │ 56 │ 34 │ 12 │
└────┴────┴────┴────┘
addr+0 +1 +2 +3
DataView API
const buf = new ArrayBuffer(8);
const view = new DataView(buf);
view.setUint32(0, 0xDEADBEEF, false);
view.setUint32(4, 0xDEADBEEF, true);
console.log(view.getUint32(0, false).toString(16));
console.log(view.getUint32(0, true).toString(16));
view.setInt8(0, -1);
view.setUint16(0, 65535, true);
view.setFloat32(0, 3.14, false);
view.setFloat64(0, Math.PI, true);
console.log(view.getFloat64(0, true));
检测当前平台字节序
function isLittleEndian() {
const buf = new ArrayBuffer(2);
new Uint16Array(buf)[0] = 0x0102;
return new Uint8Array(buf)[0] === 0x02;
}
console.log(isLittleEndian());
实战:读取 BMP 文件头(DataView)
async function parseBMPHeader(file) {
const buf = await file.arrayBuffer();
const view = new DataView(buf);
const signature = String.fromCharCode(view.getUint8(0), view.getUint8(1));
const fileSize = view.getUint32(2, true);
const reserved = view.getUint32(6, true);
const dataOffset = view.getUint32(10, true);
const dibSize = view.getUint32(14, true);
const width = view.getInt32(18, true);
const height = view.getInt32(22, true);
const planes = view.getUint16(26, true);
const bitCount = view.getUint16(28, true);
return { signature, fileSize, dataOffset, width, height, bitCount };
}
Blob/File 与 ArrayBuffer 互转
转换关系图
File extends Blob
│
├── .arrayBuffer() → ArrayBuffer
│ │
│ └── new Uint8Array(buf) → TypedArray
│
├── .text() → string(UTF-8 解码)
├── .stream() → ReadableStream
└── URL.createObjectURL(blob) → blob: URL
ArrayBuffer → Blob:new Blob([buf])
TypedArray → Blob:new Blob([typedArr])
代码示例
async function blobToBuffer(blob) {
return await blob.arrayBuffer();
}
function bufferToBlob(buf, mimeType = 'application/octet-stream') {
return new Blob([buf], { type: mimeType });
}
function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
function base64ToBuffer(b64) {
const binary = atob(b64.split(',')[1] ?? b64);
const buf = new ArrayBuffer(binary.length);
const u8 = new Uint8Array(buf);
for (let i = 0; i < binary.length; i++) {
u8[i] = binary.charCodeAt(i);
}
return buf;
}
document.querySelector('#file').addEventListener('change', async (e) => {
const file = e.target.files[0];
const buf = await file.arrayBuffer();
const u8 = new Uint8Array(buf);
console.log('文件前 4 字节:', u8.slice(0, 4));
const PNG_MAGIC = [0x89, 0x50, 0x4E, 0x47];
const isPNG = PNG_MAGIC.every((b, i) => b === u8[i]);
console.log('是 PNG 文件:', isPNG);
});
FileReader 完整 API
const reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.readAsText(blob, 'UTF-8');
reader.readAsDataURL(blob);
reader.readAsBinaryString(blob);
reader.onloadstart = (e) => {};
reader.onprogress = (e) => { console.log(e.loaded / e.total); };
reader.onload = (e) => { console.log(e.target.result); };
reader.onerror = (e) => {};
reader.onloadend = (e) => {};
reader.abort();
TextEncoder / TextDecoder
字符串 ↔ 二进制
const encoder = new TextEncoder();
const encoded = encoder.encode('Hello, 世界!');
console.log(encoded);
const buf = new Uint8Array(100);
const { read, written } = encoder.encodeInto('hello', buf);
console.log(read, written);
const decoder = new TextDecoder('UTF-8');
const decoded = decoder.decode(encoded);
console.log(decoded);
const streamDecoder = new TextDecoder('UTF-8', { fatal: true });
const chunk1 = new Uint8Array([0xE4, 0xB8]);
const chunk2 = new Uint8Array([0x96]);
console.log(streamDecoder.decode(chunk1, { stream: true }));
console.log(streamDecoder.decode(chunk2));
const gbkDecoder = new TextDecoder('GBK');
编码对比
const str = '🔥';
const utf8 = new TextEncoder().encode(str);
console.log([...utf8]);
const buf = new ArrayBuffer(str.length * 2);
const view = new Uint16Array(buf);
for (let i = 0; i < str.length; i++) {
view[i] = str.charCodeAt(i);
}
console.log([...view]);
try {
btoa('🔥');
} catch (e) {}
const toBase64 = (str) => btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (_, p1) =>
String.fromCharCode(parseInt(p1, 16))
)
);
SharedArrayBuffer 与原子操作
SharedArrayBuffer 的特殊性
const buf = new ArrayBuffer(4);
worker.postMessage(buf);
const sharedBuf = new SharedArrayBuffer(4);
worker.postMessage(sharedBuf);
Atomics 原子操作防竞态
const sharedBuf = new SharedArrayBuffer(4);
const counter = new Int32Array(sharedBuf);
Atomics.store(counter, 0, 0);
counter[0]++;
Atomics.add(counter, 0, 1);
Atomics.sub(counter, 0, 1);
const old = Atomics.compareExchange(counter, 0, 5, 10);
Atomics.wait(counter, 0, 0);
Atomics.store(counter, 0, 1);
Atomics.notify(counter, 0, 1);
Atomics.load(counter, 0);
Atomics.and/or/xor(arr, i, v);
Atomics.exchange(arr, i, v);
手写验证:BMP 头解析 & Base64 编解码
手写 1:BMP 文件头完整解析器
class BMPParser {
constructor(buffer) {
this.view = new DataView(buffer);
this.u8 = new Uint8Array(buffer);
}
parse() {
const sig = String.fromCharCode(this.u8[0], this.u8[1]);
if (sig !== 'BM') throw new Error('Not a BMP file');
return {
signature: sig,
fileSize: this.view.getUint32(2, true),
reserved1: this.view.getUint16(6, true),
reserved2: this.view.getUint16(8, true),
dataOffset: this.view.getUint32(10, true),
dibSize: this.view.getUint32(14, true),
width: this.view.getInt32(18, true),
height: this.view.getInt32(22, true),
planes: this.view.getUint16(26, true),
bitCount: this.view.getUint16(28, true),
compression: this.view.getUint32(30, true),
imageSize: this.view.getUint32(34, true),
xPPM: this.view.getInt32(38, true),
yPPM: this.view.getInt32(42, true),
colorsUsed: this.view.getUint32(46, true),
colorsImpt: this.view.getUint32(50, true),
};
}
getPixel(x, y) {
const { width, height, bitCount, dataOffset } = this.parse();
if (bitCount !== 24) throw new Error('Only 24-bit BMP supported');
const absHeight = Math.abs(height);
const rowSize = Math.floor((bitCount * width + 31) / 32) * 4;
const actualY = height > 0 ? absHeight - 1 - y : y;
const offset = dataOffset + actualY * rowSize + x * 3;
return {
b: this.u8[offset],
g: this.u8[offset + 1],
r: this.u8[offset + 2],
};
}
}
手写 2:纯 JS Base64 编解码
const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
function arrayBufferToBase64(buffer) {
const u8 = new Uint8Array(buffer);
let result = '';
let i = 0;
while (i + 2 < u8.length) {
const b0 = u8[i], b1 = u8[i + 1], b2 = u8[i + 2];
result +=
BASE64_CHARS[b0 >> 2] +
BASE64_CHARS[((b0 & 0x03) << 4) | (b1 >> 4)] +
BASE64_CHARS[((b1 & 0x0F) << 2) | (b2 >> 6)] +
BASE64_CHARS[b2 & 0x3F];
i += 3;
}
const remaining = u8.length - i;
if (remaining === 1) {
const b0 = u8[i];
result +=
BASE64_CHARS[b0 >> 2] +
BASE64_CHARS[(b0 & 0x03) << 4] + '==';
} else if (remaining === 2) {
const b0 = u8[i], b1 = u8[i + 1];
result +=
BASE64_CHARS[b0 >> 2] +
BASE64_CHARS[((b0 & 0x03) << 4) | (b1 >> 4)] +
BASE64_CHARS[(b1 & 0x0F) << 2] + '=';
}
return result;
}
function base64ToArrayBuffer(b64) {
const clean = b64.replace(/[^A-Za-z0-9+/]/g, '');
const padding = b64.endsWith('==') ? 2 : b64.endsWith('=') ? 1 : 0;
const byteLen = (clean.length * 6) / 8 - padding;
const lookup = new Uint8Array(256).fill(255);
for (let i = 0; i < BASE64_CHARS.length; i++) {
lookup[BASE64_CHARS.charCodeAt(i)] = i;
}
const buf = new ArrayBuffer(byteLen);
const u8 = new Uint8Array(buf);
let bi = 0;
for (let i = 0; i < clean.length; i += 4) {
const v0 = lookup[clean.charCodeAt(i)];
const v1 = lookup[clean.charCodeAt(i + 1)];
const v2 = lookup[clean.charCodeAt(i + 2)] ?? 0;
const v3 = lookup[clean.charCodeAt(i + 3)] ?? 0;
u8[bi++] = (v0 << 2) | (v1 >> 4);
if (bi < byteLen) u8[bi++] = ((v1 & 0x0F) << 4) | (v2 >> 2);
if (bi < byteLen) u8[bi++] = ((v2 & 0x03) << 6) | v3;
}
return buf;
}
const original = new Uint8Array([72, 101, 108, 108, 111]);
const b64 = arrayBufferToBase64(original.buffer);
console.log(b64);
const decoded = new Uint8Array(base64ToArrayBuffer(b64));
console.log([...decoded]);
console.log(new TextDecoder().decode(decoded));
深度追问
Q1:SharedArrayBuffer 和普通 ArrayBuffer 在 postMessage 时有何本质区别?
普通 ArrayBuffer 在 postMessage 时经过结构化克隆(深拷贝),每个 Worker 拿到的是独立副本;也可标记为 Transferable(postMessage(buf, [buf])),此时原线程的 buf 被 detach(byteLength 变为 0),实现零拷贝转移所有权。
SharedArrayBuffer 传递的是引用,所有线程共享同一块物理内存,无需拷贝,但必须用 Atomics 保证并发安全。同时需要服务器设置 COOP: same-origin + COEP: require-corp 响应头以防止 Spectre 攻击。
Q2:为什么 Uint8ClampedArray 专门用于 Canvas 像素数据?
Uint8ClampedArray 写入超出 [0, 255] 范围的值时会夹紧(Clamp)而不是回绕:
Uint8Array[0] = 256 → 0(回绕取模)
Uint8ClampedArray[0] = 256 → 255(夹紧)
Uint8ClampedArray[0] = -1 → 0(夹紧)
Canvas 的 ImageData.data 正是 Uint8ClampedArray,防止颜色分量溢出导致图像错误。
Q3:DataView 与直接用 TypedArray 访问字节的区别?
TypedArray 使用宿主机字节序(通常是小端),无法控制。DataView 每次读写都可以显式指定字节序(第三个参数 true = 小端),适合解析网络协议或文件格式(通常是大端)。
slice(start, end):返回新 TypedArray,拥有独立 buffer,修改不影响原数组。
subarray(begin, end):返回指向同一 buffer 的新视图(仅调整 offset/length),修改会反映到原数组,零拷贝,适合高性能场景。