TypeScript 中的 ArrayBuffer

TypeScript 中的 ArrayBuffer

本文介紹 TypeScript 中的 ArrayBuffer。

我們將逐步說明 TypeScript 中的 ArrayBuffer,從基礎到實用技巧全面講解。

YouTube Video

TypeScript 中的 ArrayBuffer

ArrayBuffer 是內建物件,用於表示二進位資料的「原始記憶體區域」。它是一個固定長度的原始緩衝區,可以在其上建立 TypedArray 或 DataView 來進行讀寫操作。

ArrayBuffer 的基本概念與特徵

ArrayBuffer 是一個固定長度的位元組序列。建立時需指定位元組大小,之後長度不可變更。

1// Create a new ArrayBuffer of 16 bytes.
2const buf = new ArrayBuffer(16);
3console.log(buf.byteLength); // 16
  • 這段程式碼建立了一個 16 位元組的空緩衝區。可以透過 byteLength 屬性來檢查大小。

TypedArray 與 DataView —— 操作緩衝區的視圖

ArrayBuffer 沒有讀寫數據的能力。因此,實際操作是透過 TypedArrayDataView 來執行,存取數據時需指定整數或浮點數等類型以及位元順序(端序)。

1// Create a buffer and a Uint8Array view over it. Then write bytes and read them.
2const buffer = new ArrayBuffer(8);
3const u8 = new Uint8Array(buffer);
4
5u8[0] = 0x41; // 'A'
6u8[1] = 0x42; // 'B'
7console.log(u8); // Uint8Array(8) [ 65, 66, 0, 0, 0, 0, 0, 0 ]
  • Uint8Array 是按位元組的陣列視圖,可以像一般陣列一樣存取。如果在同一個 ArrayBuffer 上建立多個視圖,它們會共用同一區記憶體以讀寫資料。

DataView:可指定邊界與位元組序的細緻讀寫

DataView 適合進行以位元組為單位的精細讀寫,可指定大端或小端序。

 1// Using DataView to write/read multi-byte values with endianness control.
 2const buf2 = new ArrayBuffer(8);
 3const view = new DataView(buf2);
 4
 5// Write a 32-bit integer (little-endian)
 6view.setInt32(0, 0x12345678, true);
 7
 8// Read it back as little-endian and big-endian
 9const little = view.getInt32(0, true);
10const big = view.getInt32(0, false);
11console.log(little.toString(16)); // "12345678"
12console.log(big.toString(16));    // "78563412"
  • DataView 允許你通過指定記憶體偏移來讀寫數值,非常適合用來實作需要處理網路位元順序的協定。

字串與 ArrayBuffer 的相互轉換(TextEncoder / TextDecoder)

若要在文字與二進位之間轉換,請使用 TextEncoderTextDecoder

 1// Convert string -> ArrayBuffer and back using TextEncoder/TextDecoder.
 2const encoder = new TextEncoder();
 3const decoder = new TextDecoder();
 4
 5// Unicode escape sequences
 6const str = "\u3053\u3093\u306B\u3061\u306F";
 7const encoded = encoder.encode(str); // Uint8Array
 8console.log(encoded); // Uint8Array([...])
 9
10// If you need an ArrayBuffer specifically:
11const ab = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
12console.log(ab.byteLength); // bytes length of encoded text
13
14// Decode back
15const decoded = decoder.decode(encoded);
16console.log(decoded);
  • TextEncoder.encode 會回傳一個 Uint8Array,如果你需要 ArrayBuffer,請參考它的 .buffer 屬性。透過 slice 方法,可以指定偏移量與長度來擷取所需資料。

ArrayBuffer 的切片與複製

slice 方法會回傳一個新的 ArrayBufferArrayBuffer 無法調整大小;如果需要調整,必須建立新的緩衝區並把資料複製進去。

 1// Slice an ArrayBuffer and copy to a new sized buffer.
 2const original = new Uint8Array([1,2,3,4,5]).buffer;
 3const part = original.slice(1, 4); // bytes 1..3
 4console.log(new Uint8Array(part)); // Uint8Array [ 2, 3, 4 ]
 5
 6// Resize: create a new buffer and copy existing content
 7const larger = new ArrayBuffer(10);
 8const target = new Uint8Array(larger);
 9target.set(new Uint8Array(original), 0);
10console.log(target); // first bytes filled with original data
  • 由於 slice 方法會回傳新的 ArrayBuffer,若你注重效率或需共用參考,則可使用 TypedArraysubarray 方法來指向同一個緩衝區。

TypedArray.subarray 與複製的差異

TypedArraysubarray 會回傳一個引用同一個 ArrayBuffer 的新視圖。

1// subarray shares the same underlying buffer; modifying one affects the other.
2const arr = new Uint8Array([10,20,30,40]);
3const viewSub = arr.subarray(1,3); // shares memory
4viewSub[0] = 99;
5console.log(arr); // Uint8Array [10, 99, 30, 40]
  • 共用視圖可以避免複製的開銷,但由於它們指向同一個緩衝區,需要小心可能的副作用。

緩衝區串接(結合多個 ArrayBuffer

ArrayBuffer 長度固定,結合多個緩衝區時需新建一個 ArrayBuffer 並複製資料。

 1// Concatenate multiple ArrayBuffers
 2function concatBuffers(buffers: ArrayBuffer[]): ArrayBuffer {
 3  const total = buffers.reduce((sum, b) => sum + b.byteLength, 0);
 4  const result = new Uint8Array(total);
 5  let offset = 0;
 6  for (const b of buffers) {
 7    const u8 = new Uint8Array(b);
 8    result.set(u8, offset);
 9    offset += u8.length;
10  }
11  return result.buffer;
12}
13
14const a = new Uint8Array([1,2]).buffer;
15const b = new Uint8Array([3,4,5]).buffer;
16const c = concatBuffers([a, b]);
17console.log(new Uint8Array(c)); // [1,2,3,4,5]
  • 如果你經常需要串接大量數據,像這樣預先計算總大小並只分配一次新緩衝區,效率會更高。

ArrayBuffer 傳給 Worker(Transferable 傳遞)

在瀏覽器的 postMessage 中,你可以將 ArrayBuffer 當作「可轉移」(transferable)物件來傳送。可以將擁有權直接轉移而無需複製,進而避免複製的成本。

1// Example: posting an ArrayBuffer to a Worker as a transferable object (browser)
2const worker = new Worker('worker.js');
3const bufferToSend = new Uint8Array([1,2,3,4]).buffer;
4
5// Transfer ownership to the worker (main thread no longer owns it)
6worker.postMessage(bufferToSend, [bufferToSend]);
7
8// After transfer, bufferToSend.byteLength === 0 in many browsers (detached)
9console.log(bufferToSend.byteLength); // may be 0
  • 透過將要轉移的物件以陣列方式作為 postMessage 的第二個參數,可以不複製地轉移例如 ArrayBuffer 等可轉移物件的擁有權。
  • 轉移後,原本端的緩衝區會變成「分離狀態(detached)」,無法再被存取。使用 SharedArrayBuffer 可讓多個執行緒同時存取,但此功能有安全性要求及環境限制。

Node.js 的操作方式(與 Buffer 之間的相互轉換)

在 Node.js 中,可以在 Node.js 的二進位型態 BufferArrayBuffer 之間互相轉換。這在 TypeScript 同時支援瀏覽器與 Node.js 時很實用。

 1// Convert ArrayBuffer <-> Node.js Buffer
 2// In Node.js environment:
 3const ab = new Uint8Array([10,20,30]).buffer;
 4const nodeBuffer = Buffer.from(ab); // ArrayBuffer -> Buffer
 5console.log(nodeBuffer); // <Buffer 0a 14 1e>
 6
 7const backToAb = nodeBuffer.buffer.slice(
 8    nodeBuffer.byteOffset,
 9    nodeBuffer.byteOffset + nodeBuffer.byteLength
10);
11console.log(new Uint8Array(backToAb)); // Uint8Array [10,20,30]
  • Node.js 的 Buffer.from(arrayBuffer) 通常會拷貝一份資料,但部分情況會共用記憶體,需小心位移(offset)設定。

效能考量與最佳實踐

為了最佳化效能並高效運用 ArrayBuffer,有幾個重點需要注意。下面列舉並說明實務中推薦的方法。

  • 減少複製次數 處理大量二進位資料時,建議使用 subarray(共用視圖)或 transferable 避免不必要的複製。

  • 一次分配較大的緩衝區 反覆分配小型緩衝區會增加額外負擔。如果可能,請一次分配較大的緩衝區,並在需要時使用其中的部分。

  • 明確指定位元組順序(Endianness) 處理多位元組資料時,請使用 DataView 並明確指定位元組序。大多數網路協議預設使用大端序(big-endian)。

常見使用範例

ArrayBuffer 在瀏覽器與 Node.js 中皆廣泛應用。

  1. 解析與建立二進位協議(例如用 DataView 處理標頭資訊)
  2. 媒體資料處理,例如圖片、音訊資料(例如:fetch(...).then(res => res.arrayBuffer())
  3. WebAssembly 共用記憶體(Wasm 記憶體就是基於 ArrayBuffer 運作)
  4. 將重工作委外給 Worker(以 transferable 傳遞 ArrayBuffer

以下程式碼是取得及分析二進位資料的範例。

1// Example: fetch binary data in browser and inspect first bytes
2async function fetchAndInspect(url: string) {
3  const resp = await fetch(url);
4  const ab = await resp.arrayBuffer();
5  const u8 = new Uint8Array(ab, 0, Math.min(16, ab.byteLength));
6  console.log('first bytes:', u8);
7}
  • 這段程式碼在瀏覽器中從任一 URL 取得二進位資料並顯示前幾個位元組。程式碼透過 fetch API 取得的資料以 ArrayBuffer 載入,並用 Uint8Array 檢視前 16 個位元組。

總結

ArrayBuffer 是原始記憶體資料表現,可透過 TypedArray 與 DataView 高效處理二進位操作。只要避免不必要的複製並明確指定位元組序,即可安全且高效地進行二進位處理。

您可以在我們的 YouTube 頻道上使用 Visual Studio Code 來跟隨上述文章一起學習。 請也查看我們的 YouTube 頻道。

YouTube Video