此页面旨在作为在 JavaScript 中使用二进制数据的简介。Bun 实现了许多用于处理二进制数据的数据类型和实用程序,其中大多数是 Web 标准。任何 Bun 特定的 API 都将注明为如此。
以下是一个快速“备忘单”,它兼作目录。单击左栏中的项目以跳转到该部分。
TypedArray | 提供类似于 Array 的接口,用于与二进制数据交互的类族。包括 Uint8Array 、Uint16Array 、Int8Array 等。 |
Buffer | 实现各种便捷方法的 Uint8Array 子类。与表格中的其他元素不同,这是一个 Node.js API(Bun 实现)。它不能在浏览器中使用。 |
DataView | 提供 get/set API 的类,用于将一些字节写入 ArrayBuffer 的特定字节偏移量。通常用于读取或写入二进制协议。 |
Blob | 通常表示文件的二进制数据的只读 blob。具有 MIME type 、size ,以及转换为 ArrayBuffer 、ReadableStream 和字符串的方法。 |
File | 表示文件的 Blob 子类。具有 name 和 lastModified 时间戳。Node.js v20 中有实验性支持。 |
BunFile | 仅限 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
实例,并允许你读取和操作底层数据。有两种类型的视图:类型化数组和 DataView
。
DataView
DataView
类是用于读取和操作 ArrayBuffer
中数据的较低级别接口。
下面,我们创建一个新的 DataView
,并将第一个字节设置为 5。
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
类型化数组是一系列类,它们为与 ArrayBuffer
中的数据交互提供类似于 Array
的接口。虽然 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。 |
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 中遇到的最常见的类型化数组。
它是 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
的子类。它提供了广泛的方法,包括几个类似于数组和类似于 DataView
的方法。
const buf = Buffer.from("hello world");
// => Buffer(16) [ 116, 104, 105, 115, 32, 105, 115, 32, 97, 32, 115, 116, 114, 105, 110, 103 ]
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
最初在浏览器中实现(与 ArrayBuffer
不同,后者是 JavaScript 本身的一部分),但现在在 Node 和 Bun 中受支持。
直接创建 Blob
实例并不常见。更常见的是,您会从外部源(如浏览器中的 <input type="file">
元素)或库中接收 Blob
实例。也就是说,可以从一个或多个字符串或二进制“blob 部分”创建 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.arrayBuffer(); // => ArrayBuffer (copies contents)
await blob.stream(); // => ReadableStream
BunFile
BunFile
是 Blob
的子类,用于表示磁盘上延迟加载的文件。与 File
一样,它添加了 name
和 lastModified
属性。与 File
不同,它不要求将文件加载到内存中。
const file = Bun.file("index.txt");
// => BunFile
File
仅限浏览器。Node.js 20 中的实验性支持。
File
是 Blob
的子类,添加了 name
和 lastModified
属性。它通常在浏览器中用于表示通过 <input type="file">
元素上传的文件。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 文档。
流
流是用于处理二进制数据而不一次性全部加载到内存中的重要抽象。它们通常用于读写文件、发送和接收网络请求以及处理大量数据。
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 > 流。
转换
从一种二进制格式转换为另一种二进制格式是一项常见任务。本节旨在作为参考。
从 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
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
new TextDecoder().decode(arr);
转换为 number[]
Array.from(arr);
转换为 Blob
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
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
buf.toString();
转换为 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
new Uint8Array(await blob.arrayBuffer());
转换为 DataView
new DataView(await blob.arrayBuffer());
转换为 Buffer
Buffer.from(await blob.arrayBuffer());
转换为 string
await blob.text();
转换为 number[]
Array.from(new Uint8Array(await blob.arrayBuffer()));
转换为 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);
转换为 TypedArray
// with Response
const buf = await new Response(stream).arrayBuffer();
new Uint8Array(buf);
// with Bun function
new Uint8Array(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
// with Response
new Response(stream).text();
// with Bun function
await Bun.readableStreamToText(stream);
转换为 number[]
// with Response
const buf = await new Response(stream).arrayBuffer();
Array.from(new Uint8Array(buf));
// 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();