Skip to content

本文旨在介绍如何在 JavaScript 中处理二进制数据。Bun 实现了许多用于处理二进制数据的数据类型和工具,其中大多数是 Web 标准的。任何 Bun 特定的 API 都会特别注明。

下面是一个快速"速查表",同时也作为目录。点击左列中的项目即可跳转到相应部分。

类描述
TypedArray提供用于与二进制数据交互的 Array 类似接口的一系列类。包括 Uint8Array、Uint16Array、Int8Array 等。
BufferUint8Array 的子类,实现了广泛的便捷方法。与本表中的其他元素不同,这是 Node.js API(Bun 实现了它)。它不能在浏览器中使用。
DataView提供 get/set API 用于在特定字节偏移量处向 ArrayBuffer 写入若干字节的类。常用于读取或写入二进制协议。
Blob只读二进制数据块,通常表示文件。具有 MIME type、size 以及转换为 ArrayBuffer、ReadableStream 和字符串的方法。
FileBlob 的子类,表示文件。具有 name 和 lastModified 时间戳。Node.js v20 中有实验性支持。
BunFile仅 Bun。Blob 的子类,表示磁盘上懒加载的文件。通过 Bun.file(path) 创建。

ArrayBuffer 和视图 ​

直到 2009 年,JavaScript 中还没有语言原生的方式来存储和操作二进制数据。ECMAScript v5 为此引入了一系列新机制。最基本的构建块是 ArrayBuffer,它是一个简单的数据结构,表示内存中的字节序列。

ts
// 这个缓冲区可以存储 8 个字节
const buf = new ArrayBuffer(8);

尽管名为"ArrayBuffer",但它不是数组,不支持人们可能期望的任何数组方法和运算符。事实上,无法直接读取或写入 ArrayBuffer 中的值。除了检查其大小和从中创建"切片"外,几乎无法对它做任何事情。

ts
const buf = new ArrayBuffer(8);
buf.byteLength; // => 8

const slice = buf.slice(0, 4); // 返回新的 ArrayBuffer
slice.byteLength; // => 4

要做任何有趣的事情,我们需要一个称为"视图"的结构。视图是一个_包装_ ArrayBuffer 实例的类,让你可以读取和操作底层数据。有两种类型的视图:类型化数组 和 DataView。

DataView ​

DataView 类是一个用于读取和操作 ArrayBuffer 中数据的低级接口。

下面创建一个新的 DataView 并将第一个字节设置为 3。

ts
const buf = new ArrayBuffer(4);
// [0b00000000, 0b00000000, 0b00000000, 0b00000000]

const dv = new DataView(buf);
dv.setUint8(0, 3); // 在字节偏移量 0 处写入值 3
dv.getUint8(0); // => 3
// [0b00000011, 0b00000000, 0b00000000, 0b00000000]

现在让我们在字节偏移量 1 处写入一个 Uint16。这需要两个字节。我们使用值 513,即 2 * 256 + 1;用字节表示为 00000010 00000001。

ts
dv.setUint16(1, 513);
// [0b00000011, 0b00000010, 0b00000001, 0b00000000]

console.log(dv.getUint16(1)); // => 513

我们现在已经向底层 ArrayBuffer 的前三个字节分配了值。即使第二个和第三个字节是使用 setUint16() 创建的,我们仍然可以使用 getUint8() 读取其每个组成字节。

ts
console.log(dv.getUint8(1)); // => 2
console.log(dv.getUint8(2)); // => 1

尝试写入需要比底层 ArrayBuffer 中可用空间更多的值会导致错误。下面尝试在字节偏移量 0 处写入 Float64(需要 8 个字节),但缓冲区中总共只有四个字节。

ts
dv.setFloat64(0, 3.1415);
// ^ RangeError: Out of bounds access

DataView 上提供以下方法:

GettersSetters
getBigInt64()setBigInt64()
getBigUint64()setBigUint64()
getFloat32()setFloat32()
getFloat64()setFloat64()
getInt16()setInt16()
getInt32()setInt32()
getInt8()setInt8()
getUint16()setUint16()
getUint32()setUint32()
getUint8()setUint8()

TypedArray ​

类型化数组是一系列类,提供用于与 ArrayBuffer 中数据交互的 Array 类似接口。DataView 允许你在特定偏移量处写入不同大小的数字,而 TypedArray 将底层字节解释为固定大小的数字数组。

NOTE

通常使用共享超类 `TypedArray` 来统称这一系列类。这个类在 JavaScript 中是_内部的_;你无法直接创建它的实例,`TypedArray` 在全局作用域中未定义。可以将其视为 `interface` 或抽象类。
ts
const buffer = new ArrayBuffer(3);
const arr = new Uint8Array(buffer);

// 内容初始化为零
console.log(arr); // Uint8Array(3) [0, 0, 0]

// 像数组一样分配值
arr[0] = 0;
arr[1] = 10;
arr[2] = 255;
arr[3] = 255; // 无操作,超出范围

虽然 ArrayBuffer 是通用的字节序列,但这些类型化数组类将字节解释为给定字节大小的数字数组。 顶行包含原始字节,后续行包含使用不同类型化数组类_查看_时这些字节的解释方式。

以下是类型化数组的类,以及它们如何解释 ArrayBuffer 中字节的描述:

类描述
Uint8Array每 1 个字节被解释为无符号 8 位整数。范围 0 到 255。
Uint16Array每 2 个字节被解释为无符号 16 位整数。范围 0 到 65535。
Uint32Array每 4 个字节被解释为无符号 32 位整数。范围 0 到 4294967295。
Int8Array每 1 个字节被解释为有符号 8 位整数。范围 -128 到 127。
Int16Array每 2 个字节被解释为有符号 16 位整数。范围 -32768 到 32767。
Int32Array每 4 个字节被解释为有符号 32 位整数。范围 -2147483648 到 2147483647。
Float16Array每 2 个字节被解释为 16 位浮点数。范围 -6.104e5 到 6.55e4。
Float32Array每 4 个字节被解释为 32 位浮点数。范围 -3.4e38 到 3.4e38。
Float64Array每 8 个字节被解释为 64 位浮点数。范围 -1.7e308 到 1.7e308。
BigInt64Array每 8 个字节被解释为有符号 BigInt。范围 -9223372036854775808 到 9223372036854775807(尽管 BigInt 能够表示更大的数字)。
BigUint64Array每 8 个字节被解释为无符号 BigInt。范围 0 到 18446744073709551615(尽管 BigInt 能够表示更大的数字)。
Uint8ClampedArray与 Uint8Array 相同,但在为元素分配值时自动"钳制"到 0-255 范围。

下表演示了使用不同类型化数组类查看时如何解释 ArrayBuffer 中的字节。

Byte 0Byte 1Byte 2Byte 3Byte 4Byte 5Byte 6Byte 7
ArrayBuffer0000000000000001000000100000001100000100000001010000011000000111
Uint8Array01234567
Uint16Array256 (1 * 256 + 0)770 (3 * 256 + 2)1284 (5 * 256 + 4)1798 (7 * 256 + 6)
Uint32Array50462976117835012
BigUint64Array506097522914230528n

从预定义的 ArrayBuffer 创建类型化数组:

ts
// 从 ArrayBuffer 创建类型化数组
const buf = new ArrayBuffer(10);
const arr = new Uint8Array(buf);

arr[0] = 30;
arr[1] = 60;

// 所有元素初始化为零
console.log(arr); // => Uint8Array(10) [ 30, 60, 0, 0, 0, 0, 0, 0, 0, 0 ];

如果尝试从同一个 ArrayBuffer 实例化 Uint32Array,会得到错误。

ts
const buf = new ArrayBuffer(10);
const arr = new Uint32Array(buf);
// ^ RangeError: ArrayBuffer length minus the byteOffset
// 不是元素大小的倍数

Uint32 值需要四个字节(16 位)。因为 ArrayBuffer 长 10 个字节,无法将其内容干净地划分为 4 字节的块。

要解决此问题,我们可以在 ArrayBuffer 的特定"切片"上创建类型化数组。下面的 Uint16Array 只"查看"底层 ArrayBuffer 的_前_8 个字节。要实现这一点,我们指定 byteOffset 为 0,length 为 2,表示数组要容纳的 Uint32 数字数量。

ts
// 从 ArrayBuffer 切片创建类型化数组
const buf = new ArrayBuffer(10);
const arr = new Uint32Array(buf, 0, 2);

/*
  buf    _ _ _ _ _ _ _ _ _ _    10 字节
  arr   [_______,_______]       2 个 4 字节元素
*/

arr.byteOffset; // 0
arr.length; // 2

你不需要显式创建 ArrayBuffer 实例;可以直接在类型化数组构造函数中指定长度:

ts
const arr2 = new Uint8Array(5);

// 所有元素初始化为零
// => Uint8Array(5) [0, 0, 0, 0, 0]

类型化数组也可以直接从数字数组或另一个类型化数组实例化:

ts
// 从数字数组
const arr1 = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
arr1[0]; // => 0;
arr1[7]; // => 7;

// 从另一个类型化数组
const arr2 = new Uint8Array(arr);

一般来说,类型化数组提供与普通数组相同的方法,但有一些例外。例如,push 和 pop 在类型化数组上不可用,因为它们需要调整底层 ArrayBuffer 的大小。

ts
const arr = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);

// 支持常见数组方法
arr.filter(n => n > 128); // Uint8Array(1) [255]
arr.map(n => n * 2); // Uint8Array(8) [0, 2, 4, 6, 8, 10, 12, 14]
arr.reduce((acc, n) => acc + n, 0); // 28
arr.forEach(n => console.log(n)); // 0 1 2 3 4 5 6 7
arr.every(n => n < 10); // true
arr.find(n => n > 5); // 6
arr.includes(5); // true
arr.indexOf(5); // 5

有关类型化数组的属性和方法的更多信息,请参阅 MDN 文档。

Uint8Array ​

值得特别强调 Uint8Array,因为它代表经典的"字节数组"——0 到 255 之间的 8 位无符号整数序列。这是你在 JavaScript 中最常见的类型化数组。

在 Bun 中,以及将来在其他 JavaScript 引擎中,它具有用于在字节数组与这些数组的 base64 或十六进制字符串序列化表示之间转换的方法。

ts
new Uint8Array([1, 2, 3, 4, 5]).toBase64(); // "AQIDBA=="
Uint8Array.fromBase64("AQIDBA=="); // Uint8Array(4) [1, 2, 3, 4, 5]

new Uint8Array([255, 254, 253, 252, 251]).toHex(); // "fffefdfcfb=="
Uint8Array.fromHex("fffefdfcfb"); // Uint8Array(5) [255, 254, 253, 252, 251]

它是 TextEncoder#encode 的返回值,也是 TextDecoder#decode 的输入类型,这是两个用于转换字符串和各种二进制编码(最 notably "utf-8")的工具类。

ts
const encoder = new TextEncoder();
const bytes = encoder.encode("hello world");
// => Uint8Array(11) [ 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100 ]

const decoder = new TextDecoder();
const text = decoder.decode(bytes);
// => hello world

Buffer ​

Bun 实现了 Buffer,这是一个用于处理二进制数据的 Node.js API,早于 JavaScript 规范中引入类型化数组。它后来被重新实现为 Uint8Array 的子类。它提供了广泛的方法,包括几个类似数组和 DataView 的方法。

ts
const buf = Buffer.from("hello world");
// => Buffer(11) [ 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100 ]

buf.length; // => 11
buf[0]; // => 104, 'h' 的 ascii
buf.writeUInt8(72, 0); // => 'H' 的 ascii

console.log(buf.toString());
// => Hello world

完整文档请参阅 Node.js 文档。

Blob ​

Blob 是一个 Web API,通常用于表示文件。Blob 最初是在浏览器中实现的(与作为 JavaScript 本身一部分的 ArrayBuffer 不同),但现在已在 Node 和 Bun 中得到支持。

直接创建 Blob 实例并不常见。更常见的是,你会从外部源(如浏览器中的 <input type="file"> 元素)或库接收 Blob 实例。也就是说,可以从一个或多个字符串或二进制"blob 部分"创建 Blob。

ts
const blob = new Blob(["<html>Hello</html>"], {
  type: "text/html",
});

blob.type; // => text/html
blob.size; // => 19

这些部分可以是 string、ArrayBuffer、TypedArray、DataView 或其他 Blob 实例。blob 部分按照提供的顺序连接在一起。

ts
const blob = new Blob([
  "<html>",
  new Blob(["<body>"]),
  new Uint8Array([104, 101, 108, 108, 111]), // 二进制形式的 "hello"
  "</body></html>",
]);

Blob 的内容可以异步读取为各种格式。

ts
await blob.text(); // => <html><body>hello</body></html>
await blob.bytes(); // => Uint8Array(复制内容)
await blob.arrayBuffer(); // => ArrayBuffer(复制内容)
await blob.stream(); // => ReadableStream

BunFile ​

BunFile 是 Blob 的子类,用于表示磁盘上懒加载的文件。像 File 一样,它添加了 name 和 lastModified 属性。与 File 不同,它不需要将文件加载到内存中。

ts
const file = Bun.file("index.txt");
// => BunFile

File ​

File 是 Blob 的子类,添加了 name 和 lastModified 属性。它通常在浏览器中用于表示通过 <input type="file"> 元素上传的文件。Node.js 和 Bun 实现了 File。

ts
// 在浏览器上!
// <input type="file" id="file" />

const files = document.getElementById("file").files;
// => File[]
ts
const file = new File(["<html>Hello</html>"], "index.html", {
  type: "text/html",
});

有关完整文档信息,请参阅 MDN 文档。


流 ​

流是一个重要的抽象,用于处理二进制数据而无需一次性将其全部加载到内存中。它们通常用于读写文件、发送和接收网络请求以及处理大量数据。

Bun 实现了 Web API ReadableStream 和 WritableStream。

NOTE

Bun 还实现了 `node:stream` 模块,包括 [`Readable`](https://nodejs.org/api/stream.html#stream_readable_streams)、 [`Writable`](https://nodejs.org/api/stream.html#stream_writable_streams) 和 [`Duplex`](https://nodejs.org/api/stream.html#stream_duplex_and_transform_streams)。完整文档请参阅 Node.js 文档。

创建一个简单的可读流:

ts
const stream = new ReadableStream({
  start(controller) {
    controller.enqueue("hello");
    controller.enqueue("world");
    controller.close();
  },
});

可以使用 for await 语法逐块读取此流的内容。

ts
for await (const chunk of stream) {
  console.log(chunk);
}

// => "hello"
// => "world"

有关 Bun 中流的更完整讨论,请参阅 API > 流。


转换 ​

从一种二进制格式转换为另一种是常见任务。本节旨在作为参考。

从 ArrayBuffer ​

由于 ArrayBuffer 存储构成其他二进制结构(如 TypedArray)的底层数据,下面的代码片段不是从 ArrayBuffer 转换 为另一种格式。相反,它们是使用存储在底层的数据_创建_ 新实例。

到 TypedArray ​

ts
new Uint8Array(buf);

到 DataView ​

ts
new DataView(buf);

到 Buffer ​

ts
// 在整个 ArrayBuffer 上创建 Buffer
Buffer.from(buf);

// 在 ArrayBuffer 的切片上创建 Buffer
Buffer.from(buf, 0, 10);

到 string ​

作为 UTF-8:

ts
new TextDecoder().decode(buf);

到 number[] ​

ts
Array.from(new Uint8Array(buf));

到 Blob ​

ts
new Blob([buf], { type: "text/plain" });

到 ReadableStream ​

以下代码片段创建 ReadableStream 并将整个 ArrayBuffer 作为单个块入队。

ts
new ReadableStream({
  start(controller) {
    controller.enqueue(buf);
    controller.close();
  },
});

分块"> ​

要分块流式传输 ArrayBuffer,使用 Uint8Array 视图并将每个块入队。

ts
const view = new Uint8Array(buf);
const chunkSize = 1024;

new ReadableStream({
  start(controller) {
    for (let i = 0; i < view.length; i += chunkSize) {
      controller.enqueue(view.slice(i, i + chunkSize));
    }
    controller.close();
  },
});

从 TypedArray ​

到 ArrayBuffer ​

这会检索底层 ArrayBuffer。请注意,TypedArray 可以是底层缓冲区的_切片_ 的视图,因此大小可能不同。

ts
arr.buffer;

到 DataView ​

在与 TypedArray 相同的字节范围上创建 DataView。

ts
new DataView(arr.buffer, arr.byteOffset, arr.byteLength);

到 Buffer ​

ts
Buffer.from(arr);

到 string ​

作为 UTF-8:

ts
new TextDecoder().decode(arr);

到 number[] ​

ts
Array.from(arr);

到 Blob ​

ts
// 仅当 arr 是整个底层 TypedArray 的视图时
new Blob([arr.buffer], { type: "text/plain" });

到 ReadableStream ​

ts
new ReadableStream({
  start(controller) {
    controller.enqueue(arr);
    controller.close();
  },
});

分块"> ​

要分块流式传输 ArrayBuffer,将 TypedArray 分割成块并分别入队每个块。

ts
new ReadableStream({
  start(controller) {
    for (let i = 0; i < arr.length; i += chunkSize) {
      controller.enqueue(arr.slice(i, i + chunkSize));
    }
    controller.close();
  },
});

从 DataView ​

到 ArrayBuffer ​

ts
view.buffer;

到 TypedArray ​

仅当 DataView 的 byteLength 是 TypedArray 子类的 BYTES_PER_ELEMENT 的倍数时才有效。

ts
new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
new Uint16Array(view.buffer, view.byteOffset, view.byteLength / 2);
new Uint32Array(view.buffer, view.byteOffset, view.byteLength / 4);
// 等等...

到 Buffer ​

ts
Buffer.from(view.buffer, view.byteOffset, view.byteLength);

到 string ​

作为 UTF-8:

ts
new TextDecoder().decode(view);

到 number[] ​

ts
Array.from(view);

到 Blob ​

ts
new Blob([view.buffer], { type: "text/plain" });

到 ReadableStream ​

ts
new ReadableStream({
  start(controller) {
    controller.enqueue(view.buffer);
    controller.close();
  },
});

分块"> ​

要分块流式传输 ArrayBuffer,将 DataView 分割成块并分别入队每个块。

ts
new ReadableStream({
  start(controller) {
    for (let i = 0; i < view.byteLength; i += chunkSize) {
      controller.enqueue(view.buffer.slice(i, i + chunkSize));
    }
    controller.close();
  },
});

从 Buffer ​

到 ArrayBuffer ​

ts
buf.buffer;

到 TypedArray ​

ts
new Uint8Array(buf);

到 DataView ​

ts
new DataView(buf.buffer, buf.byteOffset, buf.byteLength);

到 string ​

作为 UTF-8:

ts
buf.toString();

作为 base64:

ts
buf.toString("base64");

作为 hex:

ts
buf.toString("hex");

到 number[] ​

ts
Array.from(buf);

到 Blob ​

ts
new Blob([buf], { type: "text/plain" });

到 ReadableStream ​

ts
new ReadableStream({
  start(controller) {
    controller.enqueue(buf);
    controller.close();
  },
});

分块"> ​

要分块流式传输 ArrayBuffer,将 Buffer 分割成块并分别入队每个块。

ts
new ReadableStream({
  start(controller) {
    for (let i = 0; i < buf.length; i += chunkSize) {
      controller.enqueue(buf.slice(i, i + chunkSize));
    }
    controller.close();
  },
});

从 Blob ​

到 ArrayBuffer ​

Blob 类为此目的提供了便捷方法。

ts
await blob.arrayBuffer();

到 TypedArray ​

ts
await blob.bytes();

到 DataView ​

ts
new DataView(await blob.arrayBuffer());

到 Buffer ​

ts
Buffer.from(await blob.arrayBuffer());

到 string ​

作为 UTF-8:

ts
await blob.text();

到 number[] ​

ts
Array.from(await blob.bytes());

到 ReadableStream ​

ts
blob.stream();

从 ReadableStream ​

通常使用 Response 作为便捷的中间表示,使将 ReadableStream 转换为其他格式更容易。

ts
stream; // ReadableStream

const buffer = new Response(stream).arrayBuffer();

然而,这种方法冗长且增加了不必要的开销,降低了整体性能。Bun 实现了一组优化的便捷函数,用于将 ReadableStream 转换为各种二进制格式。

到 ArrayBuffer ​

ts
// 使用 Response
new Response(stream).arrayBuffer();

// 使用 Bun 函数
Bun.readableStreamToArrayBuffer(stream);

到 Uint8Array ​

ts
// 使用 Response
new Response(stream).bytes();

// 使用 Bun 函数
Bun.readableStreamToBytes(stream);

到 TypedArray ​

ts
// 使用 Response
const buf = await new Response(stream).arrayBuffer();
new Int8Array(buf);

// 使用 Bun 函数
new Int8Array(Bun.readableStreamToArrayBuffer(stream));

到 DataView ​

ts
// 使用 Response
const buf = await new Response(stream).arrayBuffer();
new DataView(buf);

// 使用 Bun 函数
new DataView(Bun.readableStreamToArrayBuffer(stream));

到 Buffer ​

ts
// 使用 Response
const buf = await new Response(stream).arrayBuffer();
Buffer.from(buf);

// 使用 Bun 函数
Buffer.from(Bun.readableStreamToArrayBuffer(stream));

到 string ​

作为 UTF-8:

ts
// 使用 Response
await new Response(stream).text();

// 使用 Bun 函数
await Bun.readableStreamToText(stream);

到 number[] ​

ts
// 使用 Response
const arr = await new Response(stream).bytes();
Array.from(arr);

// 使用 Bun 函数
Array.from(new Uint8Array(Bun.readableStreamToArrayBuffer(stream)));

Bun 提供用于将 ReadableStream 解析为其块的数组的工具。每个块可以是字符串、类型化数组或 ArrayBuffer。

ts
// 使用 Bun 函数
Bun.readableStreamToArray(stream);

到 Blob ​

ts
new Response(stream).blob();

到 ReadableStream ​

要将 ReadableStream 拆分为两个可以独立使用的流:

ts
const [a, b] = stream.tee();