JavaScript TypedArray 与二进制数据完全指南

掌握 ArrayBuffer、TypedArray、DataView 和二进制数据处理技术

JavaScript TypedArray 与二进制数据完全指南

TypedArray 是 JavaScript 处理二进制数据的核心工具。本文详解这些概念的用法和实际应用。

ArrayBuffer 基础

创建缓冲区

// 创建指定字节长度的缓冲区
const buffer = new ArrayBuffer(16);  // 16 字节

// 检查字节长度
buffer.byteLength;  // 16

// ArrayBuffer 不能直接读写
// 需要通过视图(View)来操作

// 复制缓冲区
const copy = buffer.slice(0, 8);  // 复制前 8 字节

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

视图概念

// TypedArray 和 DataView 是两种视图类型
const buffer = new ArrayBuffer(16);

// TypedArray 视图
const int32View = new Int32Array(buffer);

// DataView 视图
const dataView = new DataView(buffer);

// 同一个 buffer 可以有多个视图
const uint8View = new Uint8Array(buffer);
const float32View = new Float32Array(buffer);

// 它们共享同一块内存
int32View[0] = 1;
uint8View[0];  // 1(同一内存位置)

TypedArray 类型

整数类型

// 8 位整数
const int8 = new Int8Array(4);    // -128 到 127
const uint8 = new Uint8Array(4);  // 0 到 255

// 8 位无符号整数(自动截断)
const uint8c = new Uint8ClampedArray(4);  // 0-255,自动截断

// 16 位整数
const int16 = new Int16Array(4);   // -32768 到 32767
const uint16 = new Uint16Array(4); // 0 到 65535

// 32 位整数
const int32 = new Int32Array(4);   // -2^31 到 2^31-1
const uint32 = new Uint32Array(4); // 0 到 2^32-1

// 64 位整数(BigInt)
const bigInt64 = new BigInt64Array(4);
const bigUint64 = new BigUint64Array(4);

// 示例:溢出行为
const u8 = new Uint8Array(1);
u8[0] = 256;  // 变成 0(溢出)
u8[0] = -1;   // 变成 255(溢出)

const u8c = new Uint8ClampedArray(1);
u8c[0] = 256; // 变成 255(截断到最大值)
u8c[0] = -1;  // 变成 0(截断到最小值)

浮点类型

// 32 位浮点数
const float32 = new Float32Array(4);

// 64 位浮点数
const float64 = new Float64Array(4);

// 浮点数示例
const f32 = new Float32Array(1);
f32[0] = 3.14159265359;
f32[0];  // 3.1415927410125732(精度损失)

const f64 = new Float64Array(1);
f64[0] = 3.14159265359;
f64[0];  // 3.14159265359(更高精度)

创建方式

// 1. 指定长度
const arr1 = new Int32Array(4);

// 2. 从数组创建
const arr2 = new Int32Array([1, 2, 3, 4]);

// 3. 从 ArrayBuffer 创建
const buffer = new ArrayBuffer(16);
const arr3 = new Int32Array(buffer);

// 4. 带偏移和长度
const arr4 = new Int32Array(buffer, 4, 2);  // 从字节 4 开始,2 个元素

// 5. 从另一个 TypedArray 创建
const arr5 = new Float32Array(arr2);  // 类型转换

// 6. 使用 from 方法
const arr6 = Int32Array.from([1, 2, 3]);
const arr7 = Int32Array.of(1, 2, 3);

TypedArray 操作

基本操作

const arr = new Int32Array([10, 20, 30, 40]);

// 读取
arr[0];       // 10
arr.length;   // 4

// 写入
arr[0] = 100;

// 遍历
for (const value of arr) {
  console.log(value);
}

arr.forEach((value, index) => {
  console.log(index, value);
});

// 数组方法
arr.map(x => x * 2);      // Int32Array [200, 40, 60, 80]
arr.filter(x => x > 15);  // Int32Array [20, 30, 40]
arr.reduce((a, b) => a + b, 0);  // 190
arr.find(x => x > 25);    // 30
arr.indexOf(20);          // 1
arr.includes(30);         // true

复制和填充

const arr = new Int32Array(8);

// 填充
arr.fill(42);           // 全部填充 42
arr.fill(0, 2, 5);      // 索引 2-4 填充 0

// 复制到
const source = new Int32Array([1, 2, 3]);
const target = new Int32Array(5);
target.set(source);      // [1, 2, 3, 0, 0]
target.set(source, 2);   // [1, 2, 1, 2, 3]

// 复制内部
arr.copyWithin(0, 3, 5);  // 将索引 3-4 复制到索引 0

// 切片
const slice = arr.slice(1, 4);  // 新的 TypedArray

// subarray(共享内存)
const sub = arr.subarray(1, 4);  // 视图,共享同一 buffer

排序和反转

const arr = new Float32Array([3.1, 1.2, 4.5, 2.3]);

// 排序(原地修改)
arr.sort();  // [1.2, 2.3, 3.1, 4.5]

// 自定义排序
arr.sort((a, b) => b - a);  // 降序

// 反转
arr.reverse();

DataView

创建和使用

const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);

// 写入数据
view.setInt8(0, 127);
view.setInt16(1, 32767);
view.setInt32(3, 2147483647);
view.setFloat32(7, 3.14);
view.setFloat64(11, Math.PI);

// 读取数据
view.getInt8(0);      // 127
view.getInt16(1);     // 32767
view.getInt32(3);     // 2147483647
view.getFloat32(7);   // 3.14
view.getFloat64(11);  // 3.141592653589793

字节序

const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);

// 写入 0x12345678
// 大端序(Big-Endian)- 默认
view.setUint32(0, 0x12345678, false);
// 内存: 12 34 56 78

// 小端序(Little-Endian)
view.setUint32(0, 0x12345678, true);
// 内存: 78 56 34 12

// 读取时也需要指定字节序
view.getUint32(0, false);  // 大端读取
view.getUint32(0, true);   // 小端读取

// 检测系统字节序
function isLittleEndian() {
  const buffer = new ArrayBuffer(2);
  new DataView(buffer).setInt16(0, 256, true);
  return new Int16Array(buffer)[0] === 256;
}

实际应用场景

图像数据处理

// Canvas 图像数据
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, 100, 100);

// imageData.data 是 Uint8ClampedArray
const pixels = imageData.data;

// 遍历每个像素(RGBA)
for (let i = 0; i < pixels.length; i += 4) {
  const r = pixels[i];     // 红
  const g = pixels[i + 1]; // 绿
  const b = pixels[i + 2]; // 蓝
  const a = pixels[i + 3]; // 透明度

  // 灰度处理
  const gray = (r + g + b) / 3;
  pixels[i] = gray;
  pixels[i + 1] = gray;
  pixels[i + 2] = gray;
}

ctx.putImageData(imageData, 0, 0);

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

音频数据处理

// Web Audio API
const audioCtx = new AudioContext();

// 创建音频缓冲区
const sampleRate = audioCtx.sampleRate;
const duration = 2;  // 2 秒
const buffer = audioCtx.createBuffer(
  2,                    // 声道数
  sampleRate * duration, // 总采样数
  sampleRate            // 采样率
);

// 获取声道数据(Float32Array)
const channelData = buffer.getChannelData(0);

// 生成正弦波
const frequency = 440;  // A4 音符
for (let i = 0; i < channelData.length; i++) {
  channelData[i] = Math.sin(2 * Math.PI * frequency * i / sampleRate);
}

// 播放
const source = audioCtx.createBufferSource();
source.buffer = buffer;
source.connect(audioCtx.destination);
source.start();

文件操作

// 读取文件为 ArrayBuffer
async function readFileAsBuffer(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsArrayBuffer(file);
  });
}

// 解析 PNG 文件头
async function parsePNGHeader(file) {
  const buffer = await readFileAsBuffer(file);
  const view = new DataView(buffer);

  // PNG 签名:89 50 4E 47 0D 0A 1A 0A
  const signature = new Uint8Array(buffer, 0, 8);
  const expected = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];

  const isPNG = signature.every((byte, i) => byte === expected[i]);

  if (!isPNG) {
    throw new Error('Not a PNG file');
  }

  // IHDR chunk
  const width = view.getUint32(16);
  const height = view.getUint32(20);

  return { width, height };
}

网络协议

// 创建简单的二进制协议消息
function createMessage(type, payload) {
  const payloadBytes = new TextEncoder().encode(payload);
  const buffer = new ArrayBuffer(4 + payloadBytes.length);
  const view = new DataView(buffer);

  // 消息头:类型 (2字节) + 长度 (2字节)
  view.setUint16(0, type);
  view.setUint16(2, payloadBytes.length);

  // 消息体
  new Uint8Array(buffer, 4).set(payloadBytes);

  return buffer;
}

// 解析消息
function parseMessage(buffer) {
  const view = new DataView(buffer);

  const type = view.getUint16(0);
  const length = view.getUint16(2);
  const payload = new TextDecoder().decode(
    new Uint8Array(buffer, 4, length)
  );

  return { type, payload };
}

// WebSocket 发送二进制数据
const ws = new WebSocket('wss://example.com');
ws.binaryType = 'arraybuffer';

ws.onmessage = (event) => {
  const buffer = event.data;
  const message = parseMessage(buffer);
  console.log(message);
};

ws.send(createMessage(1, 'Hello'));

Base64 编解码

// ArrayBuffer 转 Base64
function bufferToBase64(buffer) {
  const bytes = new Uint8Array(buffer);
  let binary = '';
  for (let i = 0; i < bytes.length; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  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;
}

// 使用示例
const buffer = new Uint8Array([72, 101, 108, 108, 111]).buffer;
const base64 = bufferToBase64(buffer);  // 'SGVsbG8='
const decoded = base64ToBuffer(base64);

十六进制转换

// ArrayBuffer 转十六进制字符串
function bufferToHex(buffer) {
  const bytes = new Uint8Array(buffer);
  return Array.from(bytes)
    .map(b => b.toString(16).padStart(2, '0'))
    .join('');
}

// 十六进制字符串转 ArrayBuffer
function hexToBuffer(hex) {
  const bytes = new Uint8Array(hex.length / 2);
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
  }
  return bytes.buffer;
}

// 使用示例
const buffer = new Uint8Array([255, 128, 0]).buffer;
const hex = bufferToHex(buffer);  // 'ff8000'
const decoded = hexToBuffer(hex);

SharedArrayBuffer

基础用法

// 创建共享缓冲区
const sharedBuffer = new SharedArrayBuffer(16);

// 可以在 Worker 之间共享
// 主线程
const worker = new Worker('worker.js');
worker.postMessage({ buffer: sharedBuffer });

// worker.js
self.onmessage = (e) => {
  const buffer = e.data.buffer;
  const view = new Int32Array(buffer);
  view[0] = 42;  // 修改会反映到主线程
};

Atomics 操作

const buffer = new SharedArrayBuffer(16);
const view = new Int32Array(buffer);

// 原子操作
Atomics.store(view, 0, 42);      // 原子写入
Atomics.load(view, 0);           // 原子读取

// 原子加减
Atomics.add(view, 0, 5);         // 原子加法,返回旧值
Atomics.sub(view, 0, 3);         // 原子减法

// 原子位操作
Atomics.and(view, 0, 0b1111);    // 原子与
Atomics.or(view, 0, 0b1111);     // 原子或
Atomics.xor(view, 0, 0b1111);    // 原子异或

// 原子交换
Atomics.exchange(view, 0, 100);  // 设置新值,返回旧值
Atomics.compareExchange(view, 0, 100, 200);  // CAS 操作

// 等待和通知
Atomics.wait(view, 0, 0);        // 等待值变化
Atomics.notify(view, 0, 1);      // 通知等待者

最佳实践总结

TypedArray 最佳实践:
┌─────────────────────────────────────────────────────┐
│                                                     │
│   类型选择                                          │
│   ├── 图像处理:Uint8ClampedArray                  │
│   ├── 音频处理:Float32Array                       │
│   ├── 整数运算:Int32Array / Uint32Array           │
│   └── 高精度浮点:Float64Array                     │
│                                                     │
│   性能优化                                          │
│   ├── 预分配合适大小的缓冲区                       │
│   ├── 使用 subarray 避免复制                       │
│   ├── 批量操作代替逐元素操作                       │
│   └── 注意字节对齐                                 │
│                                                     │
│   使用场景                                          │
│   ├── 图形和游戏开发                               │
│   ├── 音视频处理                                   │
│   ├── 文件解析                                     │
│   └── 网络协议                                     │
│                                                     │
└─────────────────────────────────────────────────────┘
类型字节数值范围
Int8Array1-128 到 127
Uint8Array10 到 255
Int16Array2-32768 到 32767
Uint16Array20 到 65535
Int32Array4-2^31 到 2^31-1
Float32Array4IEEE 754 单精度
Float64Array8IEEE 754 双精度

掌握 TypedArray 和二进制数据处理,解锁 JavaScript 底层能力。