运行时包管理器打包器测试运行器代码检查格式化指南参考博客反馈

二进制数据

在 JavaScript 中处理二进制数据

Bun 实现了多种用于在 JavaScript 中处理二进制数据的数据类型和实用工具,其中大多数符合 Web 标准。Bun 特有的 API 会特别标注。

本速查表同时也是目录;点击左列中的类即可跳转到对应章节。

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

ArrayBuffer 和视图

直到 ECMAScript 2015(ES6)引入一系列相关机制之前,JavaScript 都没有语言原生的方式来存储和操作二进制数据。最基本的构建块是 ArrayBuffer,一种表示内存中一系列字节的数据结构。

TypeScript
// 此缓冲区可存储 8 字节
const buf = new ArrayBuffer(8);

尽管名称中包含“数组”(array),但它并不是数组,也不支持你可能期待的任何数组方法和运算符。你无法直接从 ArrayBuffer 中读取或写入值;你能做的只有检查其大小,以及从中创建“切片”。

TypeScript
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。

TypeScript
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。

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

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

底层 ArrayBuffer 的前三个字节现在都有值了。即使你使用 setUint16() 写入了第二和第三个字节,仍然可以使用 getUint8() 读取每个组成字节。

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

如果写入的值所需空间大于底层 ArrayBuffer 的可用空间,就会抛出错误。以下代码尝试在字节偏移量 0 处写入一个 Float64(需要 8 个字节),但该缓冲区的长度只有 4 个字节。

TypeScript
dv.setFloat64(0, 3.1415);
// ^ RangeError: 越界访问

DataView 支持以下方法:

TypedArray

类型化数组是一组类,提供类似数组的接口,用于访问 ArrayBuffer 中的数据。与 DataView 可以在某个偏移量写入不同大小的数字不同,TypedArray 将底层字节解释为固定大小数字的数组。

TypeScript
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 中的字节:

类描述
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.55e4 到 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 创建类型化数组:

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

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

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

从同一个 ArrayBuffer 实例化 Uint32Array 会抛出错误。

TypeScript
const buf = new ArrayBuffer(10);
const arr = new Uint32Array(buf);
//          ^  RangeError: ArrayBuffer 长度减去字节偏移量
//             不是元素大小的倍数

一个 Uint32 值需要四个字节(32 位)。由于 ArrayBuffer 的长度为 10 个字节,因此无法将其内容整齐地划分为每组 4 个字节的数据块。

要解决此问题,请在 ArrayBuffer 的特定“切片”上创建类型化数组。下面的 Uint32Array 只“查看”底层 ArrayBuffer 的前 8 个字节:byteOffset 为 0,length 为 2,表示该数组所包含的 Uint32 值的数量。

TypeScript
// 从 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 实例;可以直接将长度传递给类型化数组构造函数:

TypeScript
const arr2 = new Uint8Array(5);

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

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

TypeScript
// 从数字数组
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(arr1);

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

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

// supports common array methods
arr.filter(n => n > 5); // Uint8Array(2) [6, 7]
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 是 JavaScript 中最常见的类型化数组。它表示经典的“字节数组”:由一系列介于 0 到 255 之间的 8 位无符号整数组成。

在 Bun 中,它提供了在字节数组与其 base64 或十六进制字符串表示形式之间进行转换的方法。

TypeScript
new Uint8Array([1, 2, 3, 4, 5]).toBase64(); // "AQIDBAU="
Uint8Array.fromBase64("AQIDBAU="); // Uint8Array(5) [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"。

TypeScript
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);
// => 你好世界

Buffer

Bun 实现了 Node.js 的 Buffer,这是一个在 JavaScript 标准中引入类型化数组之前用于操作二进制数据的 API。现在它已重新实现为 Uint8Array 子类,提供多种方法,包括数组方法和类似 DataView 的方法。

TypeScript
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。它最初源自浏览器(不同于属于 JavaScript 本身的 ArrayBuffer),但 Node.js 和 Bun 也支持它。

你很少会直接创建 Blob 实例;它们通常来自外部来源(例如浏览器中的 <input type="file"> 元素)或某个库。不过,你可以使用一个或多个字符串或二进制“Blob 部分”创建 Blob。

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

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

这些部分可以是 string、ArrayBuffer、TypedArray、DataView 或其他 Blob 实例。构造函数会按你提供的顺序连接这些部分。

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

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

TypeScript
await blob.text(); // => <html><body>hello</body></html>
await blob.bytes(); // => Uint8Array (copies contents)
await blob.arrayBuffer(); // => ArrayBuffer (copies contents)
blob.stream(); // => ReadableStream

BunFile

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

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

File

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

TypeScript
// 浏览器环境
// <input type="file" id="file" />

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

请参阅 MDN 文档。


流

流让你能够处理二进制数据,而无需一次性将所有数据加载到内存中。它们通常用于读取和写入文件、发送和接收网络请求,以及处理大量数据。

Bun 实现了 Web API ReadableStream 和 WritableStream。

要创建可读流:

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

使用 for await 逐块读取流。

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

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

有关 Bun 中流的更多信息,请参见流。


格式转换

使用本节作为将一种二进制格式转换为另一种格式的参考。

从 ArrayBuffer

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

转为 TypedArray

TypeScript
new Uint8Array(buf);

转为 DataView

TypeScript
new DataView(buf);

转为 Buffer

TypeScript
// 创建覆盖整个 ArrayBuffer 的 Buffer
Buffer.from(buf);

// 创建覆盖 ArrayBuffer 某个切片的 Buffer
Buffer.from(buf, 0, 10);

转为字符串

按 UTF-8 编码:

TypeScript
new TextDecoder().decode(buf);

转为数字数组(number[])

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

转为 Blob

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

转为 ReadableStream

以下示例创建一个 ReadableStream,并将整个 ArrayBuffer 作为一个 chunk 入队。

TypeScript
new ReadableStream({
  start(controller) {
    controller.enqueue(buf);
    controller.close();
  },
});
分块读取

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

TypeScript
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

buffer 属性是底层的 ArrayBuffer。TypedArray 可以是该缓冲区某个_切片_的视图,因此两者的大小可能不同。

TypeScript
arr.buffer;

转为 DataView

要创建一个覆盖与 TypedArray 相同字节范围的 DataView:

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

转为 Buffer

TypeScript
Buffer.from(arr);

转为字符串

按 UTF-8 编码:

TypeScript
new TextDecoder().decode(arr);

转为数字数组(number[])

TypeScript
Array.from(arr);

转为 Blob

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

转为 ReadableStream

TypeScript
new ReadableStream({
  start(controller) {
    controller.enqueue(arr);
    controller.close();
  },
});
分块读取

要分块流式传输 ArrayBuffer,将类型化数组划分为多块,分别入队。

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

从 DataView

转为 ArrayBuffer

TypeScript
view.buffer;

转为 TypedArray

仅当 DataView 的 byteLength 是类型化数组元素字节大小的倍数时有效。

TypeScript
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

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

转为字符串

按 UTF-8 编码:

TypeScript
new TextDecoder().decode(view);

转为数字数组(number[])

TypeScript
Array.from(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));

转为 Blob

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

转为 ReadableStream

TypeScript
new ReadableStream({
  start(controller) {
    controller.enqueue(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
    controller.close();
  },
});
分块读取

要分块流式传输 ArrayBuffer,将 DataView 划分为多块,逐块入队。

TypeScript
const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);

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

从 Buffer

转为 ArrayBuffer

TypeScript
buf.buffer;

转为 TypedArray

TypeScript
new Uint8Array(buf);

转为 DataView

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

转为字符串

按 UTF-8:

TypeScript
buf.toString();

按 base64:

TypeScript
buf.toString("base64");

按十六进制:

TypeScript
buf.toString("hex");

转为数字数组(number[])

TypeScript
Array.from(buf);

转为 Blob

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

转为 ReadableStream

TypeScript
new ReadableStream({
  start(controller) {
    controller.enqueue(buf);
    controller.close();
  },
});
分块读取

要分块流式传输 ArrayBuffer,将 Buffer 划分为多块,逐块入队。

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

从 Blob

转为 ArrayBuffer

TypeScript
await blob.arrayBuffer();

转为 TypedArray

TypeScript
await blob.bytes();

转为 DataView

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

转为 Buffer

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

转为字符串

按 UTF-8:

TypeScript
await blob.text();

转为数字数组(number[])

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

转为 ReadableStream

TypeScript
blob.stream();

从 ReadableStream

Response 是将 ReadableStream 转换为其他格式时常用的中间类型。

TypeScript
stream; // ReadableStream

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

不过,这种方式较为冗长,并且会增加不必要的开销。Bun 为将 ReadableStream 转换为各种二进制格式实现了经过优化的便捷函数。

转为 ArrayBuffer

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

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

转为 Uint8Array

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

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

转为 TypedArray

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

// with Bun function
new Int8Array(await Bun.readableStreamToArrayBuffer(stream));

转为 DataView

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

// with Bun function
new DataView(await Bun.readableStreamToArrayBuffer(stream));

转为 Buffer

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

// with Bun function
Buffer.from(await Bun.readableStreamToArrayBuffer(stream));

转为字符串

按 UTF-8:

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

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

转为数字数组(number[])

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

// with Bun function
Array.from(new Uint8Array(await Bun.readableStreamToArrayBuffer(stream)));

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

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

转为 Blob

TypeScript
new Response(stream).blob();

转为 ReadableStream

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

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