Skip to content

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

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

描述
TypedArray提供用於與二進制數據交互的 Array 類似接口的一系列類。包括 Uint8ArrayUint16ArrayInt8Array 等。
BufferUint8Array 的子類,實現了廣泛的便捷方法。與本表中的其他元素不同,這是 Node.js API(Bun 實現了它)。它不能在瀏覽器中使用。
DataView提供 get/set API 用於在特定字節偏移量處向 ArrayBuffer 寫入若干字節的類。常用於讀取或寫入二進制協議。
Blob只讀二進制數據塊,通常表示文件。具有 MIME typesize 以及轉換為 ArrayBufferReadableStream 和字符串的方法。
FileBlob 的子類,表示文件。具有 namelastModified 時間戳。Node.js v20 中有實驗性支持。
BunFile僅 BunBlob 的子類,表示磁盤上懶加載的文件。通過 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 能夠表示更大的數字)。
Uint8ClampedArrayUint8Array 相同,但在為元素分配值時自動"鉗制"到 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 個字節。要實現這一點,我們指定 byteOffset0length2,表示數組要容納的 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);

一般來說,類型化數組提供與普通數組相同的方法,但有一些例外。例如,pushpop 在類型化數組上不可用,因為它們需要調整底層 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

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

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

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

File

FileBlob 的子類,添加了 namelastModified 屬性。它通常在瀏覽器中用於表示通過 <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 ReadableStreamWritableStream

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

僅當 DataViewbyteLengthTypedArray 子類的 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整理維護