JavaScript ArrayBuffer 与 DataView

深入解析 JavaScript 二进制数据处理:ArrayBuffer 的创建与使用、DataView 的灵活读写、TypedArray 的类型化视图,以及大端序与小端序的处理。

为什么需要二进制数据处理

JavaScript 传统上不擅长处理二进制数据。但现代 Web 应用经常需要处理图片、音视频、网络协议等二进制数据。ArrayBuffer、DataView、TypedArray 提供了高效的二进制数据处理能力。


一、ArrayBuffer

1.1 创建 ArrayBuffer

// 创建指定字节数的缓冲区
const buffer = new ArrayBuffer(8); // 8 字节
console.log(buffer.byteLength); // 8

// 不能直接读写,需要通过视图

1.2 ArrayBuffer 不可变性

ArrayBuffer 创建后大小固定,不能调整。如果需要调整,需要创建新的缓冲区:

const buffer = new ArrayBuffer(8);
const resized = new ArrayBuffer(16);
new Uint8Array(resized).set(new Uint8Array(buffer));

二、TypedArray

2.1 TypedArray 类型

类型 大小 说明
Int8Array 1 字节 有符号字节
Uint8Array 1 字节 无符号字节
Uint8ClampedArray 1 字节 截断而非溢出
Int16Array 2 字节 有符号短整型
Uint16Array 2 字节 无符号短整型
Int32Array 4 字节 有符号整型
Uint32Array 4 字节 无符号整型
Float32Array 4 字节 单精度浮点
Float64Array 8 字节 双精度浮点

2.2 创建 TypedArray

// 从 ArrayBuffer
const buffer = new ArrayBuffer(8);
const int32View = new Int32Array(buffer);

// 从普通数组
const int32Array = new Int32Array([1, 2, 3, 4]);

// 从长度
const zeros = new Int32Array(4); // [0, 0, 0, 0]

2.3 字节偏移与长度

const buffer = new ArrayBuffer(16);
const view1 = new Int32Array(buffer);
const view2 = new Int32Array(buffer, 8, 1); // 从 byte offset 8 开始,长度 1

三、DataView

3.1 为什么需要 DataView

TypedArray 对字节序(endianness)的处理取决于系统,可能导致跨平台兼容性问题。DataView 允许显式指定字节序:

const buffer = new ArrayBuffer(8);
const dataView = new DataView(buffer);

// 大端序写入
dataView.setInt32(0, 0x12345678, false); // 大端序

// 小端序读取
console.log(dataView.getInt32(0, true)); // 小端序读取

3.2 DataView 方法

// 写入
dataView.setInt8(byteOffset, value);
dataView.setUint8(byteOffset, value);
dataView.setInt16(byteOffset, value, littleEndian);
dataView.setInt32(byteOffset, value, littleEndian);
dataView.setFloat32(byteOffset, value, littleEndian);
dataView.setFloat64(byteOffset, value, littleEndian);

// 读取
dataView.getInt8(byteOffset);
dataView.getUint8(byteOffset);
dataView.getInt16(byteOffset, littleEndian);
dataView.getInt32(byteOffset, littleEndian);
dataView.getFloat32(byteOffset, littleEndian);
dataView.getFloat64(byteOffset, littleEndian);

四、字节序

4.1 大端序 vs 小端序

  • 大端序(Big Endian):高位字节在前
  • 小端序(Little Endian):低位字节在前
值 0x12345678 在内存中:
大端序:12 34 56 78
小端序:78 56 34 12

4.2 实际场景

网络协议通常用大端序,x86 处理器用小端序:

// 解析 IPv4 地址
const buffer = new ArrayBuffer(4);
new Uint8Array(buffer).set([192, 168, 1, 1]);

// 大端序读取(网络字节序)
const view = new DataView(buffer);
const ip = `${view.getUint8(0)}.${view.getUint8(1)}.${view.getUint8(2)}.${view.getUint8(3)}`;
console.log(ip); // '192.168.1.1'

五、实际应用

5.1 读取文件二进制

const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const buffer = await file.arrayBuffer();
  const view = new DataView(buffer);
  
  // 读取文件头
  const magic = view.getUint32(0, false);
  console.log('Magic number:', magic.toString(16));
});

5.2 结构化二进制数据

// 固定长度字符串(16 字节)
const NAME_SIZE = 16;

function writePerson(buffer, offset, name, age) {
  const view = new DataView(buffer);
  const nameBytes = new TextEncoder().encode(name.padEnd(NAME_SIZE, '\0'));
  new Uint8Array(buffer, offset, NAME_SIZE).set(nameBytes);
  view.setUint8(offset + NAME_SIZE, age);
}

function readPerson(buffer, offset) {
  const view = new DataView(buffer);
  const nameBytes = new Uint8Array(buffer, offset, NAME_SIZE);
  const name = new TextDecoder().decode(nameBytes).trimEnd('\0');
  const age = view.getUint8(offset + NAME_SIZE);
  return { name, age };
}

六、面试高频考点

考点 1:ArrayBuffer vs TypedArray

ArrayBuffer 是原始二进制数据容器,不能直接读写。TypedArray 是视图,提供类型化的读写能力。

考点 2:DataView vs TypedArray

DataView 可以显式指定字节序,TypedArray 的字节序取决于系统。

考点 3:字节序处理

大端序高位字节在前,小端序低位字节在前。处理跨平台数据时需要明确指定字节序。


延展阅读