二進位制資料
在 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();