Skip to content

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

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

描述
TypedArray提供用于与二进制数据交互的 Array 类似接口的一系列类。包括 Uint8ArrayUint16ArrayInt8Array 等。
BufferUint8Array 的子类,实现了广泛的便捷方法。与本表中的其他元素不同,这是 Node.js API(Bun 实现了它)。它不能在浏览器中使用。
DataView提供 get/set API 用于在特定字节偏移量处向 ArrayBuffer 写入若干字节的类。常用于读取或写入二进制协议。
Blob只读二进制数据块,通常表示文件。具有 MIME typesize 以及转换为 ArrayBufferReadableStream 和字符串的方法。
FileBlob 的子类,表示文件。具有 namelastModified 时间戳。Node.js v20 中有实验性支持。
BunFile仅 BunBlob 的子类,表示磁盘上懒加载的文件。通过 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 能够表示更大的数字)。
Uint8ClampedArrayUint8Array 相同,但在为元素分配值时自动"钳制"到 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 个字节。要实现这一点,我们指定 byteOffset0length2,表示数组要容纳的 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);

一般来说,类型化数组提供与普通数组相同的方法,但有一些例外。例如,pushpop 在类型化数组上不可用,因为它们需要调整底层 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

这些部分可以是 stringArrayBufferTypedArrayDataView 或其他 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

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

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

File

FileBlob 的子类,添加了 namelastModified 属性。它通常在浏览器中用于表示通过 <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 ReadableStreamWritableStream

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

仅当 DataViewbyteLengthTypedArray 子类的 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();

Bun学习网由www.bunjs.com.cn整理维护