`DataView` 在 TypeScript 中

`DataView` 在 TypeScript 中

本文解釋了 TypeScript 中的 DataView

我們將一步一步地說明在 TypeScript 中的 DataView,從基礎到實際應用。

YouTube Video

DataView 在 TypeScript 中

通過在 TypeScript 中使用 DataView,你可以對 ArrayBuffer 進行細緻的位元組級別的讀取和寫入操作。DataView 對於協議實現、二進位檔案分析、以及通過 WebSocket 傳送/接收二進位數據等底層二進位處理來說非常有用。

基礎概念:ArrayBuffer、TypedArray 和 DataView 之間的差異

ArrayBuffer 是用於儲存定長位元組序列的基礎資料結構。如 Uint8Array 這類的 TypedArray,是將緩衝區視為特定型別陣列的視圖,每個元素都有固定的型別。

另一方面,DataView 是一個彈性的視圖,允許你在任意偏移位置讀取和寫入任何資料類型的值。由於它不假設固定資料類型,並且可以在位元組層級進行細緻控制,非常適合分析二進位協議和進行底層處理。

以下是一個建立 ArrayBuffer,然後由其建立 Uint8ArrayDataView 的範例。

 1// Create an ArrayBuffer (8 bytes)
 2const buffer = new ArrayBuffer(8);
 3
 4// Create a Uint8Array view (easy to manipulate individual bytes)
 5const u8 = new Uint8Array(buffer);
 6u8[0] = 0x12;
 7u8[1] = 0x34;
 8
 9// Create a DataView (allows reading/writing any type at any offset)
10const dv = new DataView(buffer);
11
12// Read a 16-bit unsigned integer (big-endian assumed if second argument is omitted)
13console.log(dv.getUint16(0)); // 0x1234
  • 在此程式碼中,Uint8ArrayDataView 這兩種不同的視圖會同時作用於同一個緩衝區。利用 DataView,你可以靈活地指定任意偏移與位元組順序來讀寫數值。

DataView 的主要方法

DataView 是一個用於操作 ArrayBuffer 位元組級資料的介面,並提供了讀取與寫入各種型別(如整數、浮點數)的方法。

主要方法如下:。

  • 讀取getUint8, getInt8, getUint16, getInt16, getUint32, getInt32, getFloat32, getFloat64
  • 寫入setUint8, setInt8, setUint16, setInt16, setUint32, setInt32, setFloat32, setFloat64

所有這些方法都以「位元組偏移量」作為第一個參數:get 會讀取該位置的值,set 會將值寫入該位置。在處理 16、32 或 64 位元數據時,你也可以使用第二個參數來指定位元組順序。在實踐中,根據資料規格務必明確指定位元組順序會最安全。

下面範例說明如何以小端格式寫入一個 32 位元整數與一個 32 位元浮點數到緩衝區,並以相同格式讀取回來。

 1const buf = new ArrayBuffer(12); // Enough space for integer + float
 2const view = new DataView(buf);
 3
 4// Write a 32-bit unsigned integer at offset 0 (little-endian)
 5view.setUint32(0, 305419896, true); // 305419896 = 0x12345678
 6
 7// Read the same 32-bit unsigned integer back from offset 0 (little-endian)
 8const intVal = view.getUint32(0, true);
 9console.log(intVal); // 305419896
10
11// Write a 32-bit floating number at offset 4 (little-endian)
12view.setFloat32(4, 3.1415926, true);
13
14// Read the 32-bit floating number back from offset 4 (little-endian)
15const floatVal = view.getFloat32(4, true);
16console.log(floatVal); // 3.1415926 (approx)
  • 透過明確指定位元組順序,可以確保兼容於不同平台和二進位規格。

關於位元組順序(大小端)

有一些協議(像是網路協議)使用大端排序,但許多 CPU 或檔案格式則多採用小端設計。在 DataView 中,若第二個參數設為 true 則以小端處理,若設為 false 或省略則以大端處理。

在以下範例中,你可以看到當以大端序和小端序格式寫入相同數字時,記憶體中的位元組陣列如何改變。

 1const b = new ArrayBuffer(4);
 2const a = new Uint8Array(b);
 3const dv = new DataView(b);
 4
 5// Write in big-endian order
 6dv.setUint32(0, 0x0A0B0C0D, false); // big-endian
 7console.log([...a]); // [10, 11, 12, 13]
 8
 9// Write the same value in little-endian order
10dv.setUint32(0, 0x0A0B0C0D, true); // little-endian
11console.log([...a]); // [13, 12, 11, 10]
  • 掌握大端與小端位元組順序的變化,會讓你在分析通訊資料或二進位格式時更加容易。

字串輸入/輸出(搭配 TextEncoderDecoder 使用)

DataView 非常適合處理數值的讀寫,但不直接處理字串。通常會用 TextEncoderTextDecoder(如 UTF-8)對字串進行編碼,然後透過 Uint8Array 將其複製到緩衝區中。以下是一個將 UTF-8 字串寫入緩衝區後再讀取回來的範例。

 1const encoder = new TextEncoder();
 2const decoder = new TextDecoder();
 3
 4const str = "\u3053\u3093\u306b\u3061\u306f";
 5const encoded = encoder.encode(str); // Uint8Array (UTF-8 encoded bytes)
 6
 7// Create a buffer and write the encoded string bytes into it
 8const buf2 = new ArrayBuffer(encoded.length);
 9const u8v = new Uint8Array(buf2);
10u8v.set(encoded);
11
12// Read the bytes and decode them back into a string
13const decodedStr = decoder.decode(new Uint8Array(buf2));
14console.log(decodedStr);
  • 將字串轉為二進位資料,並根據需要在前面加上長度,即可儲存可變長度字串。

實用範例:自定義二進位格式的編碼/解碼

在下方,我們定義了一個簡單的訊息格式,包含版本號、ID以及名稱。我們實作了一個編碼流程,把訊息轉換為二進位資料;以及一個解碼流程,能從二進位資料還原出原本的物件。在此訊息格式中,第一個位元組儲存版本號,接下來的四個位元組以小端序儲存 ID,然後一個位元組表示名稱長度,最後是 UTF-8 編碼的名稱。

 1// Encode an object into a binary message
 2function encodeMessage(msg: { version: number; id: number; name: string }): ArrayBuffer {
 3  const encoder = new TextEncoder();
 4  const nameBytes = encoder.encode(msg.name);
 5  const total = 1 + 4 + 1 + nameBytes.length;
 6  const buf = new ArrayBuffer(total);
 7  const dv = new DataView(buf);
 8  let offset = 0;
 9
10  dv.setUint8(offset, msg.version); offset += 1;      // write version
11  dv.setUint32(offset, msg.id, true); offset += 4;    // write ID (little-endian)
12  dv.setUint8(offset, nameBytes.length); offset += 1; // write name length
13
14  // write name bytes
15  new Uint8Array(buf, offset).set(nameBytes);
16  return buf;
17}
18
19// Decode a binary message back into an object
20function decodeMessage(buf: ArrayBuffer) {
21  const dv = new DataView(buf);
22  let offset = 0;
23
24  const version = dv.getUint8(offset); offset += 1;   // read version
25  const id = dv.getUint32(offset, true); offset += 4; // read ID (little-endian)
26  const nameLen = dv.getUint8(offset); offset += 1;   // read name length
27
28  // extract name bytes
29  const nameBytes = new Uint8Array(buf, offset, nameLen);
30  const decoder = new TextDecoder();
31  // decode UTF-8 string
32  const name = decoder.decode(nameBytes);
33
34  return { version, id, name };
35}
36
37// Example usage
38const packed = encodeMessage({ version: 1, id: 42, name: "Alice" });
39const unpacked = decodeMessage(packed);
40console.log(unpacked); // { version: 1, id: 42, name: "Alice" }
  • 這是處理包含可變長度字串的基本訊息格式的典型實現,可應用於許多實際情境,如網路通訊或自定二進位檔案設計等。

注意事項與最佳實踐

DataView 處理二進位資料時,除了單純的讀寫以外,還有許多實務重點需要注意,如安全性、一致的位元組順序、以及正確型別處理等。尤其在處理來自外部來源的二進位資料或大型整數時,必須設計程式碼以防止誤讀和緩衝區溢位。以下是實際應用時一些值得記住的重點。

  • 邊界檢查 若偏移量或大小超出緩衝區範圍,DataView 會拋出例外。處理不可信任來源的二進位資料時,務必檢查長度。

  • 務必指定位元組順序 在程式中需明確且一致地指定小端或大端。

  • 型別一致性 JavaScript 的數值都是 64 位元 IEEE-754 浮點數。getUint32 等方法處理得宜,但沒有 getUint64,因此 64 位元整數需特別處理。

  • 64 位元整數的處理 要處理 64 位元整數,需使用 BigInt 或手動分割成高、低 32 位元兩部分。以下是一個讀取 64 位元無號整數的簡單範例。

1function getUint64(dv: DataView, offset: number, littleEndian = true): bigint {
2  const low = BigInt(dv.getUint32(offset, littleEndian));
3  const high = BigInt(dv.getUint32(offset + 4, littleEndian));
4  return littleEndian ? (high << 32n) | low : (low << 32n) | high;
5}
  • 利用 BigInt,你也可以安全地處理超過 64 位元的整數。

在 Node.js 中的使用(與 Buffer 的互通)

雖然 Buffer 在 Node.js 中很常用,但在 ArrayBufferDataView 之間轉換也非常容易。可以利用 Buffer 物件的 buffer 屬性,或 Uint8Array 建構函式來進行轉換。

1// Node.js: Buffer -> ArrayBuffer
2const nodeBuf = Buffer.from([1,2,3,4]);
3const arrBuf = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.byteLength);
4const dvNode = new DataView(arrBuf);
5console.log(dvNode.getUint16(0));
  • 這可以作為 Node.js 與瀏覽器間二進位資料交換的工具。

總結

DataView 是一種能自由讀寫二進位資料的強大機制,特別適用於需靈活控制(如指定位元組順序、隨機存取資料)等場合。結合 ArrayBufferTypedArrayDataView,能讓你在 TypeScript 中靈活且精確地處理二進位數據,支援從協議完成到檔案分析等廣泛應用。同時結合字串編碼與 64 位元整數的處理,可以實現更具實用性的二進位操作。

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

YouTube Video