二进制数据
在 JavaScript 中处理二进制数据
Bun 实现了多种用于在 JavaScript 中处理二进制数据的数据类型和实用工具,其中大多数符合 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。表示磁盘上延迟加载文件的 Blob 子类。通过 Bun.file(path) 创建。 |
ArrayBuffer 和视图
直到 ECMAScript 2015(ES6)引入一系列相关机制之前,JavaScript 都没有语言原生的方式来存储和操作二进制数据。最基本的构建块是 ArrayBuffer,一种表示内存中一系列字节的数据结构。
// 此缓冲区可存储 8 字节
const buf = new ArrayBuffer(8);
尽管名称中包含“数组”(array),但它并不是数组,也不支持你可能期待的任何数组方法和运算符。你无法直接从 ArrayBuffer 中读取或写入值;你能做的只有检查其大小,以及从中创建“切片”。
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。
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。
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 个字节),但该缓冲区的长度只有 4 个字节。
dv.setFloat64(0, 3.1415);
// ^ RangeError: 越界访问
DataView 支持以下方法:
TypedArray
类型化数组是一组类,提供类似数组的接口,用于访问 ArrayBuffer 中的数据。与 DataView 可以在某个偏移量写入不同大小的数字不同,TypedArray 将底层字节解释为固定大小数字的数组。
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 创建类型化数组:
// 从 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 会抛出错误。
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 值的数量。
// 从 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 实例;可以直接将长度传递给类型化数组构造函数:
const arr2 = new Uint8Array(5);
// 所有元素初始化为 0
// => Uint8Array(5) [0, 0, 0, 0, 0]
你也可以直接从数字数组或另一个类型化数组实例化类型化数组:
// 从数字数组
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 的大小。
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 或十六进制字符串表示形式之间进行转换的方法。
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"。
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 的方法。
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。
const blob = new Blob(["<html>Hello</html>"], {
type: "text/html",
});
blob.type; // => text/html
blob.size; // => 18
这些部分可以是 string、ArrayBuffer、TypedArray、DataView 或其他 Blob 实例。构造函数会按你提供的顺序连接这些部分。
const blob = new Blob([
"<html>",
new Blob(["<body>"]),
new Uint8Array([104, 101, 108, 108, 111]), // 二进制表示的 "hello"
"</body></html>",
]);
以各种格式异步读取 Blob 的内容。
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 不同,它不要求将文件加载到内存中。
const file = Bun.file("index.txt");
// => BunFile
File
File 是 Blob 的子类,添加了 name 和 lastModified 属性。在浏览器中,它通常用于表示通过 <input type="file"> 元素上传的文件。Node.js 和 Bun 都实现了 File。
// 浏览器环境
// <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 中流的更多信息,请参见流。
格式转换
使用本节作为将一种二进制格式转换为另一种格式的参考。
从 ArrayBuffer
由于 ArrayBuffer 存储着其他二进制结构(如 TypedArray)所依赖的数据,以下代码片段并不是将 ArrayBuffer 转换 为另一种格式。相反,它们使用底层数据_创建_一个新实例。
转为 TypedArray
new Uint8Array(buf);
转为 DataView
new DataView(buf);
转为 Buffer
// 创建覆盖整个 ArrayBuffer 的 Buffer
Buffer.from(buf);
// 创建覆盖 ArrayBuffer 某个切片的 Buffer
Buffer.from(buf, 0, 10);
转为字符串
按 UTF-8 编码:
new TextDecoder().decode(buf);
转为数字数组(number[])
Array.from(new Uint8Array(buf));
转为 Blob
new Blob([buf], { type: "text/plain" });
转为 ReadableStream
以下示例创建一个 ReadableStream,并将整个 ArrayBuffer 作为一个 chunk 入队。
new ReadableStream({
start(controller) {
controller.enqueue(buf);
controller.close();
},
});
分块读取
要分块流式传输 ArrayBuffer,使用 Uint8Array 视图并逐块入队。
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 可以是该缓冲区某个_切片_的视图,因此两者的大小可能不同。
arr.buffer;
转为 DataView
要创建一个覆盖与 TypedArray 相同字节范围的 DataView:
new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
转为 Buffer
Buffer.from(arr);
转为字符串
按 UTF-8 编码:
new TextDecoder().decode(arr);
转为数字数组(number[])
Array.from(arr);
转为 Blob
new Blob([arr], { type: "text/plain" });
转为 ReadableStream
new ReadableStream({
start(controller) {
controller.enqueue(arr);
controller.close();
},
});
分块读取
要分块流式传输 ArrayBuffer,将类型化数组划分为多块,分别入队。
new ReadableStream({
start(controller) {
for (let i = 0; i < arr.length; i += chunkSize) {
controller.enqueue(arr.slice(i, i + chunkSize));
}
controller.close();
},
});
从 DataView
转为 ArrayBuffer
view.buffer;
转为 TypedArray
仅当 DataView 的 byteLength 是类型化数组元素字节大小的倍数时有效。
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
Buffer.from(view.buffer, view.byteOffset, view.byteLength);
转为字符串
按 UTF-8 编码:
new TextDecoder().decode(view);
转为数字数组(number[])
Array.from(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
转为 Blob
new Blob([view], { type: "text/plain" });
转为 ReadableStream
new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array(view.buffer, view.byteOffset, view.byteLength));
controller.close();
},
});
分块读取
要分块流式传输 ArrayBuffer,将 DataView 划分为多块,逐块入队。
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
buf.buffer;
转为 TypedArray
new Uint8Array(buf);
转为 DataView
new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
转为字符串
按 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();
},
});
分块读取
要分块流式传输 ArrayBuffer,将 Buffer 划分为多块,逐块入队。
new ReadableStream({
start(controller) {
for (let i = 0; i < buf.length; i += chunkSize) {
controller.enqueue(buf.slice(i, i + chunkSize));
}
controller.close();
},
});
从 Blob
转为 ArrayBuffer
await blob.arrayBuffer();
转为 TypedArray
await blob.bytes();
转为 DataView
new DataView(await blob.arrayBuffer());
转为 Buffer
Buffer.from(await blob.arrayBuffer());
转为字符串
按 UTF-8:
await blob.text();
转为数字数组(number[])
Array.from(await blob.bytes());
转为 ReadableStream
blob.stream();
从 ReadableStream
Response 是将 ReadableStream 转换为其他格式时常用的中间类型。
stream; // ReadableStream
const buffer = await new Response(stream).arrayBuffer();
不过,这种方式较为冗长,并且会增加不必要的开销。Bun 为将 ReadableStream 转换为各种二进制格式实现了经过优化的便捷函数。
转为 ArrayBuffer
// 使用 Response
new Response(stream).arrayBuffer();
// 使用 Bun 函数
Bun.readableStreamToArrayBuffer(stream);
转为 Uint8Array
// 使用 Response
new Response(stream).bytes();
// 使用 Bun 函数
Bun.readableStreamToBytes(stream);
转为 TypedArray
// 使用 Response
const buf = await new Response(stream).arrayBuffer();
new Int8Array(buf);
// with Bun function
new Int8Array(await Bun.readableStreamToArrayBuffer(stream));
转为 DataView
// 使用 Response
const buf = await new Response(stream).arrayBuffer();
new DataView(buf);
// with Bun function
new DataView(await Bun.readableStreamToArrayBuffer(stream));
转为 Buffer
// 使用 Response
const buf = await new Response(stream).arrayBuffer();
Buffer.from(buf);
// with Bun function
Buffer.from(await Bun.readableStreamToArrayBuffer(stream));
转为字符串
按 UTF-8:
// 使用 Response
await new Response(stream).text();
// 使用 Bun 函数
await Bun.readableStreamToText(stream);
转为数字数组(number[])
// 使用 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。
// 使用 Bun 函数
Bun.readableStreamToArray(stream);
转为 Blob
new Response(stream).blob();
转为 ReadableStream
要将一个 ReadableStream 拆分为两个可以独立使用的流:
const [a, b] = stream.tee();