前端二进制数组完全指南:ArrayBuffer、TypedArray、DataView 一次讲透

二进制数组是 JavaScript 操作二进制数据的接口,分别有 ArrayBuffer 对象、TypedArray 视图和 DataView 视图,它们都以数组的语法处理二进制数据。

注意,二进制数组并不是真正的数组,而是类似数组的对象。

ArrayBuffer 对象

ArrayBuffer 对象代表内存之中的一段二进制数据,可以通过"视图"进行操作。"视图"部署了数组接口,可以用数组的方法操作内存。

它本身只是一个内存区域,不能直接读写内存数据,需要配合 TypedArray 视图或 DataView 视图使用。

视图

ArrayBuffer 对象作为内存区域,可以存放多种类型的数据。同一段内存,不同数据有不同的解读方式,这就叫做"视图"(view)。ArrayBuffer 有两种视图,一种是 TypedArray 视图,另一种是 DataView 视图。

ArrayBuffer 对象代表内存之中的一段二进制数据,通过"视图"进行操作。

TypedArray 视图

TypedArray 视图是以固定数据类型(如 Int8、Uint16、Float32 等)读写 ArrayBuffer 二进制数据的数组视图。

在目前标准中共有 12 种类型,其中前端开发最常用的是 Uint8ArrayInt16ArrayFloat32Array

类型 每个元素大小 说明 范围
Int8Array 1 字节 8 位有符号整数 -128 ~ 127
Uint8Array 1 字节 8 位无符号整数 0 ~ 255
Uint8ClampedArray 1 字节 8 位无符号整数(钳位) 0 ~ 255
Int16Array 2 字节 16 位有符号整数 -32768 ~ 32767
Uint16Array 2 字节 16 位无符号整数 0 ~ 65535
Int32Array 4 字节 32 位有符号整数 -2147483648 ~ 2147483647
Uint32Array 4 字节 32 位无符号整数 0 ~ 4294967295
Float16Array 2 字节 16 位浮点数 -65504 ~ 65504
Float32Array 4 字节 32 位浮点数 IEEE 754
Float64Array 8 字节 64 位浮点数 IEEE 754
BigInt64Array 8 字节 64 位有符号大整数 -2^63 ~ 2^63-1
BigUint64Array 8 字节 64 位无符号大整数 0 ~ 2^64-1

Float16ArrayFloat32ArrayFloat64Array 都是有符号的,没有对应的无符号类型。

创建 TypedArray

TypedArray 可以通过指定长度、普通数组、ArrayBuffer 或另一个 TypedArray 创建,其中基于 ArrayBuffer 创建是前端处理二进制数据最常见、最高效的方式。

指定长度 创建 TypedArray 语法为:

js 复制代码
new TypedArray(length);

例如:

js 复制代码
const arr = new Uint8Array(5);

console.log(arr);

输出:

js 复制代码
Uint8Array(5)[(0, 0, 0, 0, 0)];

使用普通数组 创建 TypedArray 语法为:

js 复制代码
new TypedArray(array);

例如:

js 复制代码
const arr = new Uint8Array([10, 20, 30]);

console.log(arr);

输出:

js 复制代码
Uint8Array(3)[(10, 20, 30)];

基于 ArrayBuffer 创建TypedArray 语法为:

js 复制代码
new TypedArray(buffer, byteOffset, length);

例如:

js 复制代码
const buffer = new ArrayBuffer(8);

const arr = new Uint8Array(buffer);

console.log(arr.length); // 8
console.log(arr.byteLength); // 8

如果需要从指定位置开始读取:

js 复制代码
const buffer = new ArrayBuffer(16);

// 从第4个字节开始,读取4个元素
const arr = new Uint8Array(buffer, 4, 4);

其中:

  • buffer:底层内存

  • 4:起始偏移(单位:字节)

  • 4:元素个数(不是字节数)

这是解析二进制文件(如 WAV、PNG)时最常见的创建方式。


使用另一个 TypedArray 创建 TypedArray 语法为

js 复制代码
new TypedArray(typedArray);

例如:

js 复制代码
const arr1 = new Uint8Array([1, 2, 3]);

const arr2 = new Uint8Array(arr1);

console.log(arr2);

输出:

js 复制代码
Uint8Array(3)[(1, 2, 3)];

注意:

这里会复制数据arr1arr2 不共享内存。


以上 4 种创建 TypedArray 的方式对比:

创建方式 示例 是否创建新的 ArrayBuffer
指定长度 new Uint8Array(8)
普通数组 new Uint8Array([1,2,3])
ArrayBuffer new Uint8Array(buffer) 否,共享已有内存
TypedArray new Uint8Array(other) 是(复制数据)

TypedArray 常用 API

TypedArray 的 API 和普通 Array 很像,但它是专门为操作二进制数据设计的。

TypedArray 提供了创建、读取、修改、遍历、复制和查找二进制数据的一系列 API。

属性
属性 作用
buffer 对应的 ArrayBuffer
byteLength 当前视图占用的字节数
byteOffset 当前视图起始偏移量(字节)
length 元素个数
BYTES_PER_ELEMENT 每个元素占用的字节数(静态属性)

示例:

js 复制代码
const arr = new Uint16Array(10); // 有 10 个元素,所以 length 为 10

console.log(arr.length); // 10
console.log(arr.byteLength); // 20
console.log(arr.byteOffset); // 0
console.log(arr.buffer); // ArrayBuffer
console.log(Uint16Array.BYTES_PER_ELEMENT); // 2

上面代码中, Uint16Array 为 16 位无符号整数,1 个元素占 2 个字节(1 个字节 = 8 位)。

因此 BYTES_PER_ELEMENT 为 2 。

因为有 10 个元素,所以 byteLength 为 10 乘以 2 ,为 20 个字节 。

读写元素

和普通数组一样,可以通过下标访问。

js 复制代码
const arr = new Int16Array(3);

arr[0] = 100;
arr[1] = -50;

console.log(arr[0]); // 100
创建和复制
  1. set() 复制另一组数据,也就是将一段内容完全复制到另一段内存。
js 复制代码
const arr = new Uint8Array(5);

arr.set([1, 2, 3]);

console.log(arr);

输出:

js 复制代码
Uint8Array(5)[(1, 2, 3, 0, 0)];

也可以指定开始位置:

js 复制代码
arr.set([8, 9], 3);

结果:

js 复制代码
[1, 2, 3, 8, 9];

  1. subarray() 创建共享同一块内存的新视图。
js 复制代码
const arr = new Uint8Array([1, 2, 3, 4]);

const sub = arr.subarray(1, 3);

console.log(sub);

输出:

js 复制代码
Uint8Array[(2, 3)];

subarray() 不会复制数据。


  1. slice() 复制数据,返回新的 TypedArray
js 复制代码
const copy = arr.slice(1, 3);

得到:

js 复制代码
Uint8Array[(2, 3)];

subarray() 不同的是:

  • slice():复制数据
  • subarray():共享内存
遍历

for...of 遍历

js 复制代码
for (const value of arr) {
  console.log(value);
}

forEach() 遍历

js 复制代码
arr.forEach((value, index) => {
  console.log(index, value);
});

entries()

js 复制代码
for (const [i, v] of arr.entries()) {
  console.log(i, v);
}

keys()

js 复制代码
for (const key of arr.keys()) {
  console.log(key);
}

values()

js 复制代码
for (const value of arr.values()) {
  console.log(value);
}
查找

普通数组中的查找方法都可以运用在 TypedArray 中。

js 复制代码
arr.includes(10);
arr.indexOf(10);
arr.lastIndexOf(10);
arr.find((v) => v > 100);
arr.findIndex((v) => v > 100);
转换

普通数组中的转换方法对 TypedArray 也适用

js 复制代码
arr.join(",");
arr.toString();

Array.from(arr);

[...arr];

例如:

js 复制代码
const arr = new Uint8Array([1, 2, 3]);

console.log(Array.from(arr));

输出:

js 复制代码
[1, 2, 3];
迭代方法

普通数组中的迭代方法对 TypedArray 也适用

js 复制代码
arr.map(...)
arr.filter(...)
arr.reduce(...)
arr.reduceRight(...)
arr.some(...)
arr.every(...)

例如:

js 复制代码
const arr = new Uint8Array([1, 2, 3]);

const result = arr.map((x) => x * 2);

console.log(result);

输出:

js 复制代码
Uint8Array[(2, 4, 6)];
排序

TypedArray 的排序方法与普通数组的排序方法一样。

js 复制代码
arr.sort();
arr.reverse();

例如:

js 复制代码
const arr = new Uint8Array([3, 1, 2]);

arr.sort();

console.log(arr);

输出:

js 复制代码
Uint8Array[(1, 2, 3)];
填充

普通数组的填充方法对 TypedArray 也适用。

js 复制代码
arr.fill(100);

输出:

js 复制代码
[100, 100, 100];
判断

普通数组中的判断方法对 TypedArray 也适用。

js 复制代码
arr.includes(10);

返回:

js 复制代码
true;
false;
与普通 Array 相比的区别

TypedArray 支持很多与 Array 相同的方法,但也有一些限制:

  • 元素类型固定 ,例如 Uint8Array 只能存储 0 ~ 255 的整数。
  • 长度固定 ,创建后不能使用 push()pop()shift()unshift()splice() 等会改变长度的方法。

例如:

js 复制代码
const arr = new Uint8Array([1, 2, 3]);

arr.push(4); // ❌ TypeError

这是因为 TypedArray 的底层 ArrayBuffer 大小是固定的,不能像普通数组那样动态扩容。

复合视图

在介绍 DataView 视图前,先介绍复合视图。

复合视图就是多个不同的视图(TypedArrayDataView)同时查看、操作同一个 ArrayBuffer

先看普通视图

假设有一块 8 字节内存:

js 复制代码
const buffer = new ArrayBuffer(8);

const view = new Uint8Array(buffer);

此时 buffer 内存中,只有 1 个 Uint8Array 视图。

再看复合视图

同一块内存:

js 复制代码
const buffer = new ArrayBuffer(8);

const uint8 = new Uint8Array(buffer);
const uint16 = new Uint16Array(buffer);
const dataView = new DataView(buffer);

此时 buffer 内存中,有 3 个视图,分别为 Uint8ArrayUint16ArrayDataView。这就是复合视图。

多个视图共享同一块内存。

最直观的例子

js 复制代码
const buffer = new ArrayBuffer(4);

const uint8 = new Uint8Array(buffer);
const uint16 = new Uint16Array(buffer);

uint8[0] = 1;
uint8[1] = 0;

console.log(uint16[0]);

输出:

js 复制代码
1;

因为 uint8uint16 共享同一块内存,所以 uint8 中的修改,也会影响 uint16 中的数据。

类比:同一本书

ArrayBuffer 想象成一本书。

按字阅读:

复制代码
今
天
天
气
不
错

相当于:

js 复制代码
Uint8Array;

按词阅读:

复制代码
今天
天气
不错

相当于:

js 复制代码
Uint16Array;

任意位置阅读:

复制代码
从第3个字开始读

相当于:

js 复制代码
DataView;

这三个人读的是:

复制代码
同一本书

只是阅读方式不同。

这就是复合视图的本质。

为什么需要复合视图?

解析二进制文件时非常常见。

例如 WAV 文件由文件头、元数据和 PCM 数据组成。

js 复制代码
function readString(view, offset, length) {
  let str = "";
  for (let i = 0; i < length; i++) {
    str += String.fromCharCode(view.getUint8(offset + i));
  }
  return str;
}

async function main() {
  const response = await fetch("/5s_16k_mono_16bit_440hz.wav");
  const buffer = await response.arrayBuffer();

  const view = new DataView(buffer);
  const chunkID = readString(view, 0, 4); // "RIFF"
  const pcm = new Int16Array(buffer, 44); // PCM 数据
}

main();

在上面代码中,使用 DataView 读取文件头,使用 Int16Array 读取 PCM 数据,它们共享同一个 ArrayBuffer。

这是复合视图在实际开发中的典型用法。

DataView 视图

DataView 是一种可以按照任意数据类型和任意字节偏移量灵活读写 ArrayBuffer 的视图。

js 复制代码
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);

view.setInt16(0, 1000);
console.log(view.getInt16(0)); // 1000

TypedArray 的区别为:

ini 复制代码
TypedArray  = 整块内存按同一种类型查看
DataView    = 内存中的任意位置按任意类型查看

可以说 DataView 是一个"万能二进制读取器",可以从 ArrayBuffer 的任意位置,以任意数据类型读取或写入数据,非常适合解析 WAV、PNG、网络协议等二进制格式。

创建 DataView

DataView 必须基于 ArrayBuffer 创建,语法为:

js 复制代码
new DataView(buffer, byteOffset?, byteLength?)
  • buffer:要操作的 ArrayBuffer
  • byteOffset:起始偏移(字节),默认 0
  • byteLength:视图长度(字节),默认到 buffer 末尾
js 复制代码
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
const view2 = new DataView(buffer, 2, 4); // 从第2字节开始,读4字节

DataView 常用 API

DataView 的 API 很有规律:getXxx() 用于读取数据,setXxx() 用于写入数据,Xxx 表示数据类型(如 Uint16Float32),再配合 bufferbyteOffsetbyteLength 三个属性,就可以灵活地操作任意二进制数据。

属性
属性 作用
buffer 对应的 ArrayBuffer
byteLength 视图占用的字节数
byteOffset 视图在 buffer 中的起始偏移
读取方法

从指定偏移量以指定类型读取数据,语法为 view.get<Type>(byteOffset, littleEndian?)

方法 作用 字节数
getInt8() 读取 8 位有符号整数 1
getUint8() 读取 8 位无符号整数 1
getInt16() 读取 16 位有符号整数 2
getUint16() 读取 16 位无符号整数 2
getInt32() 读取 32 位有符号整数 4
getUint32() 读取 32 位无符号整数 4
getFloat16() 读取 16 位浮点数 2
getFloat32() 读取 32 位浮点数 4
getFloat64() 读取 64 位浮点数 8
getBigInt64() 读取 64 位整数(BigInt) 8
getBigUint64() 读取 64 位无符号整数(BigInt) 8

littleEndian 默认为 false(大端序),设 true 为小端序。

写入方法

在指定偏移量以指定类型写入数据,语法为 view.set<Type>(byteOffset, value, littleEndian?)

方法 作用 字节数
setInt8() 写入 8 位有符号整数 1
setUint8() 写入 8 位无符号整数 1
setInt16() 写入 16 位有符号整数 2
setUint16() 写入 16 位无符号整数 2
setInt32() 写入 32 位有符号整数 4
setUint32() 写入 32 位无符号整数 4
setFloat16() 写入 16 位浮点数 2
setFloat32() 写入 32 位浮点数 4
setFloat64() 写入 64 位浮点数 8
setBigInt64() 写入 64 位整数(BigInt) 8
setBigUint64() 写入 64 位无符号整数(BigInt) 8

示例:

js 复制代码
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);

// 写入
view.setUint8(0, 82); // 第0字节写入 'R' (ASCII 82)
view.setUint16(2, 44100, true); // 第2字节写入 44100(小端序)

// 读取
console.log(view.getUint8(0)); // 82
console.log(view.getUint16(2, true)); // 44100

关于 littleEndian

除了 getInt8()getUint8()setInt8()setUint8()(它们只有 1 个字节,不涉及字节顺序),其他多字节方法都有一个可选参数:

js 复制代码
view.getUint32(offset, true);

其中:

arduino 复制代码
true

表示:

按小端字节序(Little Endian)读取或写入。

字节序 (Endianness)指多字节数据在内存中的存放顺序。以占据 4 个字节的 16 进制数 0x12345678 为例:

ini 复制代码
大端序(Big Endian):高位字节在前
地址:  [0]   [1]   [2]   [3]
数据:  0x12  0x34  0x56  0x78  → 符合人类阅读习惯

小端序(Little Endian):低位字节在前
地址:  [0]   [1]   [2]   [3]
数据:  0x78  0x56  0x34  0x12  → CPU 运算更高效
  • 大端序:高字节存低地址,像"从左到右"读数字,网络协议常用
  • 小端序:低字节存低地址,x86/ARM 等主流 CPU 默认的字节序。

下面的函数可以用来判断,当前视图是小端字节序,还是大端字节序。

js 复制代码
const BIG_ENDIAN = Symbol("BIG_ENDIAN");
const LITTLE_ENDIAN = Symbol("LITTLE_ENDIAN");

function getPlatformEndianness() {
  let arr32 = Uint32Array.of(0x12345678);
  let arr8 = new Uint8Array(arr32.buffer);
  switch (arr8[0] * 0x1000000 + arr8[1] * 0x10000 + arr8[2] * 0x100 + arr8[3]) {
    case 0x12345678:
      return BIG_ENDIAN;
    case 0x78563412:
      return LITTLE_ENDIAN;
    default:
      throw new Error("Unknown endianness");
  }
}

总结

  1. ArrayBuffer --- 原始二进制内存,不能直接读写,需通过视图操作。
  2. TypedArray --- 固定类型的数组视图,4 种创建方式(基于 ArrayBuffer 最高效),支持丰富的数组 API,但长度和类型均固定。
  3. 复合视图 --- 多个视图共享同一块 ArrayBuffer,是解析二进制文件的核心技巧。类比"同一本书,按字、按词、任意位置三种读法"。
  4. DataView --- 万能二进制读取器,可在任意偏移量以任意类型读写,getXxx()/setXxx() 方法覆盖所有数据类型。
  5. 字节序 --- 大端序(高位在前,网络协议常用)vs 小端序(低位在前,主流 CPU 默认小端序)。DataView 的多字节方法通过 littleEndian 参数控制;用 Uint32Array + Uint8Array 的复合视图可探测平台字节序。
  6. getPlatformEndianness() --- 将 4 字节按大端序权重(×256³、×256²、×256、×1)重组,比对结果判断平台字节序。

核心思想:一块内存,多种解读 --- 这正是二进制数组的精髓。

相关推荐
张风捷特烈1 小时前
Flutter 类库大揭秘#02 | path_provider 各平台实现
前端·flutter
铁皮饭盒2 小时前
26年bunjs, elysia+pg一把梭, redis都省了
前端·javascript·后端
lichenyang45315 小时前
Docker 学习笔记(一):为什么需要镜像、容器和仓库?
前端
kyriewen15 小时前
别再对着 TypeScript 报错发呆了:我把 10 个最常见的红色波浪线翻译成了人话
前端·javascript·typescript
IT_陈寒15 小时前
SpringBoot自动配置的坑,我的API突然就404了
前端·人工智能·后端
free3516 小时前
从 0 实现一个 Tiny JavaScript VM:项目架构拆解
javascript
奇奇怪怪的16 小时前
Embedding 模型 10+ 横向评测
前端
陈广亮16 小时前
Monorepo 从 0 到 1 实操指南 2026 版:pnpm catalogs + Turborepo 2.x + changesets 全链路
前端