JavaScript 處理二進位資料 TypedArray, ArrayBuffer, DataView
在早期的 JavaScript 中,主要是用來處理網頁上的文字內容,對於原始的二進位資料 (Binary Data) 並沒有很好的支援。但是隨著 HTML5 的發展,Web 應用程式變得越來越強大,我們開始需要在瀏覽器中處理音訊、影片、WebSockets 原始數據,甚至是 WebGL 的 3D 繪圖。
為了滿足這些高效能操作二進位資料的需求,JavaScript 引入了 TypedArray (具型別陣列) 的概念。
這篇文章將帶你深入了解 JavaScript 如何透過 ArrayBuffer、TypedArray 和 DataView 來操作二進位資料。
核心概念概述
在深入語法之前,我們需要先理解這三個核心角色的關係:
- ArrayBuffer (緩衝區):代表一段原始的、固定長度的二進位記憶體空間。你不能直接操作它裡面的內容。
- TypedArray (具型別陣列):是一個視圖 (View),它讓你用特定的格式(例如:每個元素都是 8 位元無號整數
Uint8)來讀寫ArrayBuffer的內容。 - DataView (資料視圖):也是一種視圖,但它更靈活。它允許你在同一個 Buffer 中混合讀寫不同類型的數據(例如:開頭讀一個 16 位元整數,接著讀一個 32 位元浮點數),並且可以控制位元組順序 (Endianness)。
ArrayBuffer 是記憶體中的「實際資料」,而 TypedArray 和 DataView 是用來「解讀」這些資料的「眼鏡」。ArrayBuffer
ArrayBuffer 是用來分配一段連續記憶體空間的物件。
語法
// 分配 16 bytes (位元組) 的記憶體空間
const buffer = new ArrayBuffer(16);
console.log(buffer.byteLength); // 16
ArrayBuffer 實例只有一個屬性 byteLength,用來取得它的大小。特別注意的是,你無法直接讀寫 ArrayBuffer 的內容,必須透過 View (TypedArray 或 DataView) 來操作。
TypedArray (具型別陣列)
TypedArray 提供了一種類似一般陣列的操作介面,但它的每個元素都是固定型別且連續的。這使得它在效能上比一般的 JavaScript Array [] 快得多。
常見的 TypedArray 類型包括:
| 名稱 | 每個元素大小 (Bytes) | 說明 | 數值範圍 |
|---|---|---|---|
| Uint8Array | 1 | 8 位元無號整數 | 0 ~ 255 |
| Uint16Array | 2 | 16 位元無號整數 | 0 ~ 65535 |
| Uint32Array | 4 | 32 位元無號整數 | 0 ~ 4294967295 |
| Int8Array | 1 | 8 位元有號整數 | -128 ~ 127 |
| Float32Array | 4 | 32 位元浮點數 | 1.2 x 10^-38 ~ 3.4 x 10^38 |
| Float64Array | 8 | 64 位元浮點數 | 5.0 x 10^-324 ~ 1.8 x 10^308 |
| Uint8ClampedArray | 1 | 8 位元固定無號整數 | 0 ~ 255 (特別用於 Canvas ImageData) |
建立 TypedArray
有幾種方式可以建立 TypedArray:
1. 直接指定長度
// 建立一個有 8 個元素的 Uint8Array
// 這會自動建立一個 8 bytes 的 ArrayBuffer
const uint8 = new Uint8Array(8);
console.log(uint8.length); // 8 (元素個數)
console.log(uint8.byteLength); // 8 (總 Bytes 數)
// 預設都會初始化為 0
console.log(uint8[0]); // 0
2. 使用一般的 Array 或其他可迭代物件
// 將一般陣列轉換為 Uint8Array
const uint8 = new Uint8Array([10, 20, 30, 40]);
console.log(uint8[0]); // 10
console.log(uint8.length); // 4
3. 共用 ArrayBuffer (同一個記憶體,不同視圖)
這是一個非常強大的特性。我們可以創建多個 Views 指向同一個 ArrayBuffer。
const buffer = new ArrayBuffer(4); // 4 bytes
// 用 Uint8Array 看這個 buffer (視為 4 個 8-bit 整數)
const uint8 = new Uint8Array(buffer);
uint8[0] = 1;
uint8[1] = 2;
uint8[2] = 3;
uint8[3] = 4;
console.log(uint8); // Uint8Array(4) [1, 2, 3, 4]
// 用 Uint16Array 看同一個 buffer (視為 2 個 16-bit 整數)
const uint16 = new Uint16Array(buffer);
// 結果會是什麼?這取決於系統的 Endianness (通常是 Little Endian)
// 記憶體中是: 01 02 03 04 (hex: 0x01, 0x02, 0x03, 0x04)
// Little Endian 下:
// uint16[0] 是 0x0201 (十進位 513)
// uint16[1] 是 0x0403 (十進位 1027)
console.log(uint16[0]); // 513
TypedArray 的特性與方法
TypedArray 擁有許多與一般 Array 相同的方法,例如:
map(), filter(), reduce(), forEach(), find(), some(), slice() 等等。
但有幾個主要區別:
- 長度固定:一旦建立,長度就不能改變。所以沒有
push(),pop(),splice()等會改變長度的方法。 - 型別限制:如果你嘗試存入超出範圍的值,它不會報錯,但會發生溢位 (Overflow) 或截斷。
const uint8 = new Uint8Array(1);
// 超出 255,會取餘數 (256 變 0, 257 變 1)
uint8[0] = 256;
console.log(uint8[0]); // 0
uint8[0] = 257;
console.log(uint8[0]); // 1
set() 方法
用於從另一個陣列複製數值到 TypedArray。
const uint8 = new Uint8Array(5);
uint8.set([1, 2], 1); // 從 index 1 開始寫入
console.log(uint8); // Uint8Array(5) [0, 1, 2, 0, 0]
subarray() 方法
回傳一個新的 TypedArray,但共用相同的 ArrayBuffer記憶體空間。這跟一般陣列的 slice() 不同(slice 會複製一份新的)。
const ui8 = new Uint8Array([10, 20, 30, 40]);
const sub = ui8.subarray(1, 3); // 取 index 1 到 3 (不含 3)
console.log(sub); // Uint8Array(2) [20, 30]
sub[0] = 99; // 修改子視圖
console.log(ui8); // 原本的陣列也被修改了: Uint8Array(4) [10, 99, 30, 40]
DataView
TypedArray 適合處理「全部都是同一種類型」的資料。但如果我們面對的是一個結構化的二進位檔(例如檔頭 Header 有 2 bytes ID,接著 4 bytes 長度,再接著字串內容...),使用 DataView 會更方便。
DataView 提供了 get 和 set 方法來讀寫不同型態的數值,並且可以指定 Byte Order (Endianness)。
語法
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
// 寫入資料
view.setInt8(0, 127); // 在 byte offset 0 寫入 int8
view.setUint16(1, 5000); // 在 byte offset 1 寫入 uint16
// 讀取資料
console.log(view.getInt8(0)); // 127
console.log(view.getUint16(1)); // 5000
Endianness (位元組順序)
在多字節數字(例如 16-bit, 32-bit)中,位元組的排列順序有兩種:
- Big-endian (大端序):高位元組在前(人類讀習慣的順序)。
- Little-endian (小端序):低位元組在前(Intel x86 架構常用)。
DataView 的方法預設使用 Big-endian。若要使用 Little-endian,需要在方法的最後一個參數傳入 true。
const buffer = new ArrayBuffer(2);
const view = new DataView(buffer);
// 寫入 258 (Hex: 0x0102)
// Big-endian: [01, 02]
// Little-endian: [02, 01]
view.setUint16(0, 258); // 預設 Big-endian
console.log(new Uint8Array(buffer)); // [1, 2]
view.setUint16(0, 258, true); // 使用 Little-endian
console.log(new Uint8Array(buffer)); // [2, 1]
實務應用範例
Canvas 影像處理
HTML5 Canvas 的 ImageData 其實底層就是一個 Uint8ClampedArray。每個像素佔用 4 個 bytes (R, G, B, A)。
// 假設 ctx 是一個 Canvas 2D Context
const imageData = ctx.getImageData(0, 0, 100, 100);
const data = imageData.data; // 這是一個 Uint8ClampedArray
// data 裡的排列是 [R, G, B, A, R, G, B, A, ...]
// 讓我們把整張圖變成紅色
for (let i = 0; i < data.length; i += 4) {
data[i] = 255; // Red
data[i + 1] = 0; // Green
data[i + 2] = 0; // Blue
// data[i + 3] 是 Alpha (透明度),保持不變
}
ctx.putImageData(imageData, 0, 0);
為什麼這裡用 Uint8ClampedArray?
因為如果我們計算出的顏色是 300,一般 Uint8Array 會變成 300 % 256 = 44(顏色跑掉)。而 Uint8ClampedArray 會將其「鉗制 (Clamp)」在 255(最大值),確保顏色運算是正確的(0 ~ 255)。
Fetch API 讀取二進位檔案
當我們下載一張圖片或一個二進位檔案時,可以使用 ArrayBuffer 來處理。
fetch('image.png')
.then((response) => response.arrayBuffer()) // 告訴 fetch 我們要 ArrayBuffer
.then((buffer) => {
console.log(`下載了 ${buffer.byteLength} bytes 的資料`);
// 做一些處理,例如轉成 Blob 顯示
const blob = new Blob([buffer], { type: 'image/png' });
const url = URL.createObjectURL(blob);
// document.getElementById('img').src = url;
});
合併多個 ArrayBuffer
有時候我們需要將多個二進位片段合併成一個,這必須透過 TypedArray 來建立一個夠大的新 Buffer 並複製內容。
function concatenate(buffer1, buffer2) {
// 建立一個新的 TypedArray,大小是兩者之和
const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
// 複製第一個 buffer
tmp.set(new Uint8Array(buffer1), 0);
// 複製第二個 buffer,位置接在第一個後面
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
return tmp.buffer; // 回傳底層的 ArrayBuffer
}
總結
- ArrayBuffer 是原始的二進位資料容器。
- TypedArray (如
Uint8Array) 是用來操作同一類型數據的高效能視圖,操作類似 Array 但長度固定。 - DataView 是用來操作混合類型數據的靈活視圖,支援 Endianness 設定。
- 這些 API 在 WebGL、Canvas 影像處理、檔案操作 (File API) 以及網路傳輸 (Fetch/WebSocket) 中扮演著關鍵角色。
掌握這些工具,能讓你更有效地處理 JavaScript 中的底層資料!