Bun

二进制数据

此页面旨在作为在 JavaScript 中使用二进制数据的简介。Bun 实现了许多用于处理二进制数据的数据类型和实用程序,其中大多数是 Web 标准。任何 Bun 特定的 API 都将注明为如此。

以下是一个快速“备忘单”,它兼作目录。单击左栏中的项目以跳转到该部分。

TypedArray提供类似于 Array 的接口,用于与二进制数据交互的类族。包括 Uint8ArrayUint16ArrayInt8Array 等。
Buffer实现各种便捷方法的 Uint8Array 子类。与表格中的其他元素不同,这是一个 Node.js API(Bun 实现)。它不能在浏览器中使用。
DataView提供 get/set API 的类,用于将一些字节写入 ArrayBuffer 的特定字节偏移量。通常用于读取或写入二进制协议。
Blob通常表示文件的二进制数据的只读 blob。具有 MIME typesize,以及转换为 ArrayBufferReadableStream 和字符串的方法。
File表示文件的 Blob 子类。具有 namelastModified 时间戳。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 能够表示更大的数字)。
Uint8ClampedArrayUint8Array 相同,但在为元素赋值时自动“限制”到 0-255 的范围。

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

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

从预定义的 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 个字节。为了实现这些,我们指定了一个 byteOffset0 和一个 length2,它表示我们的数组要容纳的 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);

从广义上讲,类型化数组提供与常规数组相同的方法,但有一些例外。例如,类型化数组上不可用 pushpop,因为它们需要调整底层 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

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

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

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

File

仅限浏览器。Node.js 20 中的实验性支持。

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

Bun 还实现了 node:stream 模块,包括 ReadableWritableDuplex。有关完整文档,请参阅 Node.js 文档。

要创建简单的可读流

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

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