本页面旨在介绍在 JavaScript 中处理二进制数据。Bun 实现了一系列用于处理二进制数据的数据类型和实用工具,其中大部分是 Web 标准。任何 Bun 特有的 API 都将另行注明。
下面是一个快速的“备忘单”,它同时也是目录。点击左栏的项目可跳转到相应部分。
TypedArray (类型化数组) | 一组类,提供类似 `Array` 的接口来处理二进制数据。包括 `Uint8Array`、`Uint16Array`、`Int8Array` 等。 |
Buffer | `Uint8Array` 的一个子类,实现了广泛的便捷方法。与其他表格项不同,这是一个 Node.js API(Bun 已实现)。它不能在浏览器中使用。 |
DataView (数据视图) | 一个类,提供 `get/set` API,用于在特定的字节偏移量处向 `ArrayBuffer` 写入一定数量的字节。常用于读取或写入二进制协议。 |
Blob | 一个只读的二进制数据块,通常代表一个文件。它有一个 MIME `type`、一个 `size`,以及转换为 `ArrayBuffer`、`ReadableStream` 和字符串的方法。 |
File (文件) | `Blob` 的一个子类,代表一个文件。它有一个 `name` 和 `lastModified` 时间戳。Node.js v20 中有实验性支持。 |
BunFile (Bun 文件) | 仅限 Bun。`Blob` 的一个子类,代表磁盘上一个延迟加载的文件。使用 `Bun.file(path)` 创建。 |
ArrayBuffer (数组缓冲区) 和视图
直到 2009 年,JavaScript 中都没有一种原生的方式来存储和操作二进制数据。ECMAScript v5 引入了一系列新的机制来实现这一点。最基本的构建块是 `ArrayBuffer`,它是一个表示内存中字节序列的简单数据结构。
// this buffer can store 8 bytes
const buf = new ArrayBuffer(8);
尽管名称如此,它并不是一个数组,也不支持任何预期的数组方法和运算符。事实上,无法直接从 `ArrayBuffer` 读取或写入值。你能用它做的除了检查大小和从中创建“切片”之外,几乎没有什么了。
const buf = new ArrayBuffer(8);
buf.byteLength; // => 8
const slice = buf.slice(0, 4); // returns new ArrayBuffer
slice.byteLength; // => 4
要进行任何有趣的操作,我们需要一个称为“视图”的构造。视图是一个类,它*包装*了一个 `ArrayBuffer` 实例,并允许你读取和操作底层数据。视图有两种类型:*类型化数组* (`typed arrays`) 和 `DataView`。
DataView (数据视图)
`DataView` 类提供了一个低级接口,用于读取和操作 `ArrayBuffer` 中的数据。
下面我们创建一个新的 `DataView` 并将第一个字节设置为 3。
const buf = new ArrayBuffer(4);
// [0b00000000, 0b00000000, 0b00000000, 0b00000000]
const dv = new DataView(buf);
dv.setUint8(0, 3); // write value 3 at byte offset 0
dv.getUint8(0); // => 3
// [0b00000011, 0b00000000, 0b00000000, 0b00000000]
现在,我们在字节偏移量 `1` 处写入一个 `Uint16`。这需要两个字节。我们使用的值是 `513`,也就是 `2 * 256 + 1`;以字节表示,是 `00000010 00000001`。
dv.setUint16(1, 513);
// [0b00000011, 0b00000010, 0b00000001, 0b00000000]
console.log(dv.getUint16(1)); // => 513
我们现在已经为底层 `ArrayBuffer` 的前三个字节赋值了。即使第二个和第三个字节是使用 `setUint16()` 创建的,我们仍然可以使用 `getUint8()` 读取其每个组成字节。
console.log(dv.getUint8(1)); // => 2
console.log(dv.getUint8(2)); // => 1
尝试写入需要比底层 `ArrayBuffer` 中可用空间更多空间的值得会引发错误。下面我们尝试在字节偏移量 `0` 处写入一个 `Float64`(需要 8 个字节),但缓冲区总共只有四个字节。
dv.setFloat64(0, 3.1415);
// ^ RangeError: Out of bounds access
以下方法可在 `DataView` 上使用:
TypedArray (类型化数组)
类型化数组是一组类,它们提供了一个类似 `Array` 的接口来处理 `ArrayBuffer` 中的数据。`DataView` 允许你在特定偏移量写入不同大小的数字,而 `TypedArray` 则将底层字节解释为固定大小数字的数组。
注意 — 通常将这类类统称为 `TypedArray`。这个类是 JavaScript 的*内部*类;你不能直接创建它的实例,而且 `TypedArray` 不在全局作用域中定义。将其视为一个 `interface` 或抽象类。
const buffer = new ArrayBuffer(3);
const arr = new Uint8Array(buffer);
// contents are initialized to zero
console.log(arr); // Uint8Array(3) [0, 0, 0]
// assign values like an array
arr[0] = 0;
arr[1] = 10;
arr[2] = 255;
arr[3] = 255; // no-op, out of bounds
虽然 `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` 中的字节时,这些字节是如何被解释的。
ArrayBuffer (数组缓冲区) | 00000000 | 00000001 | 00000010 | 00000011 | 00000100 | 00000101 | 00000110 | 00000111 |
Uint8Array | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
Uint16Array | 256 (1 * 256 + 0) | 770 (3 * 256 + 2) | 1284 (5 * 256 + 4) | 1798 (7 * 256 + 6) | ||||
Uint32Array | 50462976 | 117835012 | ||||||
BigUint64Array | 506097522914230528n | |||||||
从预定义的 `ArrayBuffer` 创建类型化数组
// create typed array from ArrayBuffer
const buf = new ArrayBuffer(10);
const arr = new Uint8Array(buf);
arr[0] = 30;
arr[1] = 60;
// all elements are initialized to zero
console.log(arr); // => Uint8Array(10) [ 30, 60, 0, 0, 0, 0, 0, 0, 0, 0 ];
如果我们尝试从同一个 `ArrayBuffer` 实例化一个 `Uint32Array`,我们会得到一个错误。
const buf = new ArrayBuffer(10);
const arr = new Uint32Array(buf);
// ^ RangeError: ArrayBuffer length minus the byteOffset
// is not a multiple of the element size
一个 `Uint32` 值需要四个字节(16 位)。因为 `ArrayBuffer` 长度为 10 字节,无法将其内容整齐地划分为 4 字节块。
为了解决这个问题,我们可以创建跨越 `ArrayBuffer` 特定“切片”的类型化数组。下面的 `Uint16Array` 只*查看*底层 `ArrayBuffer` 的*前* 8 个字节。为了实现这一点,我们指定 `byteOffset` 为 `0`,`length` 为 `2`,这表示我们希望数组包含的 `Uint32` 数量。
// create typed array from ArrayBuffer slice
const buf = new ArrayBuffer(10);
const arr = new Uint32Array(buf, 0, 2);
/*
buf _ _ _ _ _ _ _ _ _ _ 10 bytes
arr [_______,_______] 2 4-byte elements
*/
arr.byteOffset; // 0
arr.length; // 2
你不需要显式创建 `ArrayBuffer` 实例;你可以在类型化数组构造函数中直接指定长度。
const arr2 = new Uint8Array(5);
// all elements are initialized to zero
// => Uint8Array(5) [0, 0, 0, 0, 0]
类型化数组也可以直接从数字数组或另一个类型化数组实例化。
// from an array of numbers
const arr1 = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
arr1[0]; // => 0;
arr1[7]; // => 7;
// from another typed array
const arr2 = new Uint8Array(arr);
广义来说,类型化数组提供与常规数组相同的许多方法,但有少数例外。例如,类型化数组没有 `push` 和 `pop` 方法,因为它们会需要改变底层 `ArrayBuffer` 的大小。
const arr = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
// supports common array methods
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 或十六进制字符串序列化表示之间进行转换的方法。
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 的输入类型。这两个工具类旨在转换字符串和各种二进制编码,尤其是 `“utf-8”`。
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` 的子类。它提供了广泛的方法,包括一些类似 Array 和 `DataView` 的方法。
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, ascii for 'h'
buf.writeUInt8(72, 0); // => ascii for 'H'
console.log(buf.toString());
// => Hello world
有关完整文档,请参阅 Node.js 文档。
Blob
Blob 是一个 Web API,常用于表示文件。`Blob` 最初是在浏览器中实现的(与作为 JavaScript 本身的 `ArrayBuffer` 不同),但现在在 Node 和 Bun 中都得到了支持。
直接创建 `Blob` 实例并不常见。更常见的情况是,你会从外部源(如浏览器中的 `` 元素)或库接收 `Blob` 实例。不过,也可以从一个或多个字符串或二进制“blob parts”创建 `Blob`。
const blob = new Blob(["<html>Hello</html>"], {
type: "text/html",
});
blob.type; // => text/html
blob.size; // => 19
这些部分可以是 `string`、`ArrayBuffer`、`TypedArray`、`DataView` 或其他 `Blob` 实例。blob 部分按提供的顺序连接在一起。
const blob = new Blob([
"<html>",
new Blob(["<body>"]),
new Uint8Array([104, 101, 108, 108, 111]), // "hello" in binary
"</body></html>",
]);
Blob 的内容可以异步地以各种格式读取。
await blob.text(); // => <html><body>hello</body></html>
await blob.bytes(); // => Uint8Array (copies contents)
await blob.arrayBuffer(); // => ArrayBuffer (copies contents)
await blob.stream(); // => ReadableStream
BunFile (Bun 文件)
BunFile 是 `Blob` 的一个子类,用于表示磁盘上一个延迟加载的文件。与 `File` 一样,它添加了 `name` 和 `lastModified` 属性。与 `File` 不同,它不需要将文件加载到内存中。
const file = Bun.file("index.txt");
// => BunFile
File (文件)
仅限浏览器。Node.js 20 中有实验性支持。
File 是 `Blob` 的一个子类,它添加了 `name` 和 `lastModified` 属性。它常用于浏览器中表示通过 `` 元素上传的文件。Node.js 和 Bun 都实现了 `File`。
// on browser!
// <input type="file" id="file" />
const files = document.getElementById("file").files;
// => File[]
const file = new File(["<html>Hello</html>"], "index.html", {
type: "text/html",
});
有关完整文档信息,请参阅 MDN 文档。
Streams (流)
流是处理二进制数据而不一次性将其全部加载到内存中的重要抽象。它们常用于读取和写入文件、发送和接收网络请求以及处理大量数据。
Bun 实现 Web API ReadableStream 和 WritableStream。
创建一个简单的可读流
const stream = new ReadableStream({
start(controller) {
controller.enqueue("hello");
controller.enqueue("world");
controller.close();
},
});
可以使用 `for await` 语法逐块读取此流的内容。
for await (const chunk of stream) {
console.log(chunk);
// => "hello"
// => "world"
}
有关 Bun 中流的更完整讨论,请参阅 API > Streams。
转换
将一种二进制格式转换为另一种格式是一项常见任务。本节旨在作为参考。
从 ArrayBuffer (数组缓冲区)
由于 `ArrayBuffer` 存储了 `TypedArray` 等其他二进制结构底层的数据,因此下面的代码片段并非*从* `ArrayBuffer` *转换*到另一种格式。相反,它们是*使用*存储的底层数据*创建*了一个新实例。
转换为 TypedArray
new Uint8Array(buf);
转换为 DataView
new DataView(buf);
转换为 Buffer
// create Buffer over entire ArrayBuffer
Buffer.from(buf);
// create Buffer over a slice of the ArrayBuffer
Buffer.from(buf, 0, 10);
转换为 string
作为 UTF-8
new TextDecoder().decode(buf);
转换为 number[]
Array.from(new Uint8Array(buf));
转换为 Blob
new Blob([buf], { type: "text/plain" });
转换为 ReadableStream
以下代码片段创建一个 `ReadableStream`,并将整个 `ArrayBuffer` 作为单个块排队。
new ReadableStream({
start(controller) {
controller.enqueue(buf);
controller.close();
},
});
分块处理
从 TypedArray (类型化数组)
转换为 ArrayBuffer
这会检索底层 `ArrayBuffer`。请注意,`TypedArray` 可以是底层缓冲区的*切片*的视图,因此大小可能不同。
arr.buffer;
转换为 DataView
创建跨越与 `TypedArray` 相同字节范围的 `DataView`。
new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
转换为 Buffer
Buffer.from(arr);
转换为 string
作为 UTF-8
new TextDecoder().decode(arr);
转换为 number[]
Array.from(arr);
转换为 Blob
// only if arr is a view of its entire backing TypedArray
new Blob([arr.buffer], { type: "text/plain" });
转换为 ReadableStream
new ReadableStream({
start(controller) {
controller.enqueue(arr);
controller.close();
},
});
分块处理
从 DataView (数据视图)
转换为 ArrayBuffer
view.buffer;
转换为 TypedArray
仅当 `DataView` 的 `byteLength` 是 `TypedArray` 子类的 `BYTES_PER_ELEMENT` 的倍数时才有效。
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);
// etc...
转换为 Buffer
Buffer.from(view.buffer, view.byteOffset, view.byteLength);
转换为 string
作为 UTF-8
new TextDecoder().decode(view);
转换为 number[]
Array.from(view);
转换为 Blob
new Blob([view.buffer], { type: "text/plain" });
转换为 ReadableStream
new ReadableStream({
start(controller) {
controller.enqueue(view.buffer);
controller.close();
},
});
分块处理
从 Buffer (缓冲区)
转换为 ArrayBuffer
buf.buffer;
转换为 TypedArray
new Uint8Array(buf);
转换为 DataView
new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
转换为 string
作为 UTF-8
buf.toString();
作为 base64
buf.toString("base64");
作为十六进制
buf.toString("hex");
转换为 number[]
Array.from(buf);
转换为 Blob
new Blob([buf], { type: "text/plain" });
转换为 ReadableStream
new ReadableStream({
start(controller) {
controller.enqueue(buf);
controller.close();
},
});
分块处理
从 Blob
转换为 ArrayBuffer
`Blob` 类为此目的提供了一个便捷方法。
await blob.arrayBuffer();
转换为 TypedArray
await blob.bytes();
转换为 DataView
new DataView(await blob.arrayBuffer());
转换为 Buffer
Buffer.from(await blob.arrayBuffer());
转换为 string
作为 UTF-8
await blob.text();
转换为 number[]
Array.from(await blob.bytes());
转换为 ReadableStream
blob.stream();
从 ReadableStream (可读流)
通常使用 Response 作为方便的中间表示,以便更容易地将 `ReadableStream` 转换为其他格式。
stream; // ReadableStream
const buffer = new Response(stream).arrayBuffer();
然而,这种方法很冗长,并且会增加不必要的开销,从而降低整体性能。Bun 实现了一组优化的便捷函数,用于将 `ReadableStream` 转换为各种二进制格式。
转换为 ArrayBuffer
// with Response
new Response(stream).arrayBuffer();
// with Bun function
Bun.readableStreamToArrayBuffer(stream);
转换为 Uint8Array
// with Response
new Response(stream).bytes();
// with Bun function
Bun.readableStreamToBytes(stream);
转换为 TypedArray
// with Response
const buf = await new Response(stream).arrayBuffer();
new Int8Array(buf);
// with Bun function
new Int8Array(Bun.readableStreamToArrayBuffer(stream));
转换为 DataView
// with Response
const buf = await new Response(stream).arrayBuffer();
new DataView(buf);
// with Bun function
new DataView(Bun.readableStreamToArrayBuffer(stream));
转换为 Buffer
// with Response
const buf = await new Response(stream).arrayBuffer();
Buffer.from(buf);
// with Bun function
Buffer.from(Bun.readableStreamToArrayBuffer(stream));
转换为 string
作为 UTF-8
// with Response
await new Response(stream).text();
// with Bun function
await Bun.readableStreamToText(stream);
转换为 number[]
// with Response
const arr = await new Response(stream).bytes();
Array.from(arr);
// with Bun function
Array.from(new Uint8Array(Bun.readableStreamToArrayBuffer(stream)));
Bun 提供了一个实用函数来解析 `ReadableStream` 并将其块作为数组返回。每个块可能是一个字符串、类型化数组或 `ArrayBuffer`。
// with Bun function
Bun.readableStreamToArray(stream);
转换为 Blob
new Response(stream).blob();
转换为 ReadableStream
将 `ReadableStream` 分割成两个可以独立消费的流
const [a, b] = stream.tee();