Skip to content

本文旨在介紹如何在 JavaScript 中處理二進制數據。Bun 實現了許多用於處理二進制數據的數據類型和工具,其中大多數是 Web 標准的。任何 Bun 特定的 API 都會特別注明。

下面是一個快速"速查表",同時也作為目錄。點擊左列中的項目即可跳轉到相應部分。

類描述
TypedArray提供用於與二進制數據交互的 Array 類似接口的一系列類。包括 Uint8Array、Uint16Array、Int8Array 等。
BufferUint8Array 的子類,實現了廣泛的便捷方法。與本表中的其他元素不同,這是 Node.js API(Bun 實現了它)。它不能在瀏覽器中使用。
DataView提供 get/set API 用於在特定字節偏移量處向 ArrayBuffer 寫入若干字節的類。常用於讀取或寫入二進制協議。
Blob只讀二進制數據塊,通常表示文件。具有 MIME type、size 以及轉換為 ArrayBuffer、ReadableStream 和字符串的方法。
FileBlob 的子類,表示文件。具有 name 和 lastModified 時間戳。Node.js v20 中有實驗性支持。
BunFile僅 Bun。Blob 的子類,表示磁盤上懶加載的文件。通過 Bun.file(path) 創建。

ArrayBuffer 和視圖 ​

直到 2009 年,JavaScript 中還沒有語言原生的方式來存儲和操作二進制數據。ECMAScript v5 為此引入了一系列新機制。最基本的構建塊是 ArrayBuffer,它是一個簡單的數據結構,表示內存中的字節序列。

ts
// 這個緩沖區可以存儲 8 個字節
const buf = new ArrayBuffer(8);

盡管名為"ArrayBuffer",但它不是數組,不支持人們可能期望的任何數組方法和運算符。事實上,無法直接讀取或寫入 ArrayBuffer 中的值。除了檢查其大小和從中創建"切片"外,幾乎無法對它做任何事情。

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

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

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

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

我們現在已經向底層 ArrayBuffer 的前三個字節分配了值。即使第二個和第三個字節是使用 setUint16() 創建的,我們仍然可以使用 getUint8() 讀取其每個組成字節。

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

嘗試寫入需要比底層 ArrayBuffer 中可用空間更多的值會導致錯誤。下面嘗試在字節偏移量 0 處寫入 Float64(需要 8 個字節),但緩沖區中總共只有四個字節。

ts
dv.setFloat64(0, 3.1415);
// ^ RangeError: Out of bounds access

DataView 上提供以下方法:

GettersSetters
getBigInt64()setBigInt64()
getBigUint64()setBigUint64()
getFloat32()setFloat32()
getFloat64()setFloat64()
getInt16()setInt16()
getInt32()setInt32()
getInt8()setInt8()
getUint16()setUint16()
getUint32()setUint32()
getUint8()setUint8()

TypedArray ​

類型化數組是一系列類,提供用於與 ArrayBuffer 中數據交互的 Array 類似接口。DataView 允許你在特定偏移量處寫入不同大小的數字,而 TypedArray 將底層字節解釋為固定大小的數字數組。

NOTE

通常使用共享超類 `TypedArray` 來統稱這一系列類。這個類在 JavaScript 中是_內部的_;你無法直接創建它的實例,`TypedArray` 在全局作用域中未定義。可以將其視為 `interface` 或抽象類。
ts
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 是通用的字節序列,但這些類型化數組類將字節解釋為給定字節大小的數字數組。 頂行包含原始字節,後續行包含使用不同類型化數組類_查看_時這些字節的解釋方式。

以下是類型化數組的類,以及它們如何解釋 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 中的字節。

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

從預定義的 ArrayBuffer 創建類型化數組:

ts
// 從 ArrayBuffer 創建類型化數組
const buf = new ArrayBuffer(10);
const arr = new Uint8Array(buf);

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

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

如果嘗試從同一個 ArrayBuffer 實例化 Uint32Array,會得到錯誤。

ts
const buf = new ArrayBuffer(10);
const arr = new Uint32Array(buf);
// ^ RangeError: ArrayBuffer length minus the byteOffset
// 不是元素大小的倍數

Uint32 值需要四個字節(16 位)。因為 ArrayBuffer 長 10 個字節,無法將其內容干淨地劃分為 4 字節的塊。

要解決此問題,我們可以在 ArrayBuffer 的特定"切片"上創建類型化數組。下面的 Uint16Array 只"查看"底層 ArrayBuffer 的_前_8 個字節。要實現這一點,我們指定 byteOffset 為 0,length 為 2,表示數組要容納的 Uint32 數字數量。

ts
// 從 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 實例;可以直接在類型化數組構造函數中指定長度:

ts
const arr2 = new Uint8Array(5);

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

類型化數組也可以直接從數字數組或另一個類型化數組實例化:

ts
// 從數字數組
const arr1 = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
arr1[0]; // => 0;
arr1[7]; // => 7;

// 從另一個類型化數組
const arr2 = new Uint8Array(arr);

一般來說,類型化數組提供與普通數組相同的方法,但有一些例外。例如,push 和 pop 在類型化數組上不可用,因為它們需要調整底層 ArrayBuffer 的大小。

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

// 支持常見數組方法
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 或十六進制字符串序列化表示之間轉換的方法。

ts
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 的輸入類型,這是兩個用於轉換字符串和各種二進制編碼(最 notably "utf-8")的工具類。

ts
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 的方法。

ts
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,通常用於表示文件。Blob 最初是在瀏覽器中實現的(與作為 JavaScript 本身一部分的 ArrayBuffer 不同),但現在已在 Node 和 Bun 中得到支持。

直接創建 Blob 實例並不常見。更常見的是,你會從外部源(如瀏覽器中的 <input type="file"> 元素)或庫接收 Blob 實例。也就是說,可以從一個或多個字符串或二進制"blob 部分"創建 Blob。

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

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

這些部分可以是 string、ArrayBuffer、TypedArray、DataView 或其他 Blob 實例。blob 部分按照提供的順序連接在一起。

ts
const blob = new Blob([
  "<html>",
  new Blob(["<body>"]),
  new Uint8Array([104, 101, 108, 108, 111]), // 二進制形式的 "hello"
  "</body></html>",
]);

Blob 的內容可以異步讀取為各種格式。

ts
await blob.text(); // => <html><body>hello</body></html>
await blob.bytes(); // => Uint8Array(復制內容)
await blob.arrayBuffer(); // => ArrayBuffer(復制內容)
await blob.stream(); // => ReadableStream

BunFile ​

BunFile 是 Blob 的子類,用於表示磁盤上懶加載的文件。像 File 一樣,它添加了 name 和 lastModified 屬性。與 File 不同,它不需要將文件加載到內存中。

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

File ​

File 是 Blob 的子類,添加了 name 和 lastModified 屬性。它通常在瀏覽器中用於表示通過 <input type="file"> 元素上傳的文件。Node.js 和 Bun 實現了 File。

ts
// 在瀏覽器上!
// <input type="file" id="file" />

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

有關完整文檔信息,請參閱 MDN 文檔。


流 ​

流是一個重要的抽象,用於處理二進制數據而無需一次性將其全部加載到內存中。它們通常用於讀寫文件、發送和接收網絡請求以及處理大量數據。

Bun 實現了 Web API ReadableStream 和 WritableStream。

NOTE

Bun 還實現了 `node:stream` 模塊,包括 [`Readable`](https://nodejs.org/api/stream.html#stream_readable_streams)、 [`Writable`](https://nodejs.org/api/stream.html#stream_writable_streams) 和 [`Duplex`](https://nodejs.org/api/stream.html#stream_duplex_and_transform_streams)。完整文檔請參閱 Node.js 文檔。

創建一個簡單的可讀流:

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

可以使用 for await 語法逐塊讀取此流的內容。

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

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

有關 Bun 中流的更完整討論,請參閱 API > 流。


轉換 ​

從一種二進制格式轉換為另一種是常見任務。本節旨在作為參考。

從 ArrayBuffer ​

由於 ArrayBuffer 存儲構成其他二進制結構(如 TypedArray)的底層數據,下面的代碼片段不是從 ArrayBuffer 轉換 為另一種格式。相反,它們是使用存儲在底層的數據_創建_ 新實例。

到 TypedArray ​

ts
new Uint8Array(buf);

到 DataView ​

ts
new DataView(buf);

到 Buffer ​

ts
// 在整個 ArrayBuffer 上創建 Buffer
Buffer.from(buf);

// 在 ArrayBuffer 的切片上創建 Buffer
Buffer.from(buf, 0, 10);

到 string ​

作為 UTF-8:

ts
new TextDecoder().decode(buf);

到 number[] ​

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

到 Blob ​

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

到 ReadableStream ​

以下代碼片段創建 ReadableStream 並將整個 ArrayBuffer 作為單個塊入隊。

ts
new ReadableStream({
  start(controller) {
    controller.enqueue(buf);
    controller.close();
  },
});

分塊"> ​

要分塊流式傳輸 ArrayBuffer,使用 Uint8Array 視圖並將每個塊入隊。

ts
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 ​

這會檢索底層 ArrayBuffer。請注意,TypedArray 可以是底層緩沖區的_切片_ 的視圖,因此大小可能不同。

ts
arr.buffer;

到 DataView ​

在與 TypedArray 相同的字節范圍上創建 DataView。

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

到 Buffer ​

ts
Buffer.from(arr);

到 string ​

作為 UTF-8:

ts
new TextDecoder().decode(arr);

到 number[] ​

ts
Array.from(arr);

到 Blob ​

ts
// 僅當 arr 是整個底層 TypedArray 的視圖時
new Blob([arr.buffer], { type: "text/plain" });

到 ReadableStream ​

ts
new ReadableStream({
  start(controller) {
    controller.enqueue(arr);
    controller.close();
  },
});

分塊"> ​

要分塊流式傳輸 ArrayBuffer,將 TypedArray 分割成塊並分別入隊每個塊。

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

從 DataView ​

到 ArrayBuffer ​

ts
view.buffer;

到 TypedArray ​

僅當 DataView 的 byteLength 是 TypedArray 子類的 BYTES_PER_ELEMENT 的倍數時才有效。

ts
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 ​

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

到 string ​

作為 UTF-8:

ts
new TextDecoder().decode(view);

到 number[] ​

ts
Array.from(view);

到 Blob ​

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

到 ReadableStream ​

ts
new ReadableStream({
  start(controller) {
    controller.enqueue(view.buffer);
    controller.close();
  },
});

分塊"> ​

要分塊流式傳輸 ArrayBuffer,將 DataView 分割成塊並分別入隊每個塊。

ts
new ReadableStream({
  start(controller) {
    for (let i = 0; i < view.byteLength; i += chunkSize) {
      controller.enqueue(view.buffer.slice(i, i + chunkSize));
    }
    controller.close();
  },
});

從 Buffer ​

到 ArrayBuffer ​

ts
buf.buffer;

到 TypedArray ​

ts
new Uint8Array(buf);

到 DataView ​

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

到 string ​

作為 UTF-8:

ts
buf.toString();

作為 base64:

ts
buf.toString("base64");

作為 hex:

ts
buf.toString("hex");

到 number[] ​

ts
Array.from(buf);

到 Blob ​

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

到 ReadableStream ​

ts
new ReadableStream({
  start(controller) {
    controller.enqueue(buf);
    controller.close();
  },
});

分塊"> ​

要分塊流式傳輸 ArrayBuffer,將 Buffer 分割成塊並分別入隊每個塊。

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

從 Blob ​

到 ArrayBuffer ​

Blob 類為此目的提供了便捷方法。

ts
await blob.arrayBuffer();

到 TypedArray ​

ts
await blob.bytes();

到 DataView ​

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

到 Buffer ​

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

到 string ​

作為 UTF-8:

ts
await blob.text();

到 number[] ​

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

到 ReadableStream ​

ts
blob.stream();

從 ReadableStream ​

通常使用 Response 作為便捷的中間表示,使將 ReadableStream 轉換為其他格式更容易。

ts
stream; // ReadableStream

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

然而,這種方法冗長且增加了不必要的開銷,降低了整體性能。Bun 實現了一組優化的便捷函數,用於將 ReadableStream 轉換為各種二進制格式。

到 ArrayBuffer ​

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

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

到 Uint8Array ​

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

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

到 TypedArray ​

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

// 使用 Bun 函數
new Int8Array(Bun.readableStreamToArrayBuffer(stream));

到 DataView ​

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

// 使用 Bun 函數
new DataView(Bun.readableStreamToArrayBuffer(stream));

到 Buffer ​

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

// 使用 Bun 函數
Buffer.from(Bun.readableStreamToArrayBuffer(stream));

到 string ​

作為 UTF-8:

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

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

到 number[] ​

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

// 使用 Bun 函數
Array.from(new Uint8Array(Bun.readableStreamToArrayBuffer(stream)));

Bun 提供用於將 ReadableStream 解析為其塊的數組的工具。每個塊可以是字符串、類型化數組或 ArrayBuffer。

ts
// 使用 Bun 函數
Bun.readableStreamToArray(stream);

到 Blob ​

ts
new Response(stream).blob();

到 ReadableStream ​

要將 ReadableStream 拆分為兩個可以獨立使用的流:

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

Bun學習網由www.bunjs.com.cn整理維護