博客 / 詳情

返回

ArrayBuffer 二進制數據

在 Web 開發中,當我們處理文件時(創建,上傳,下載),經常會遇到二進制數據。另一個典型的應用場景是圖像處理。

與其他語言相比,JavaScript 中的二進制數據是以非標準方式實現的。

1. 創建二進制數據

基本的二進制對象是 ArrayBuffer —— 對固定長度的連續內存空間的引用。

let buffer = new ArrayBuffer(16); // 創建一個長度為 16 的 buffer
alert(buffer.byteLength); // 16

它會分配一個 16 字節的連續內存空間,並用 0 進行預填充

注意:ArrayBuffer 不是某種東西的數組
讓我們先澄清一個可能的誤區。ArrayBufferArray 沒有任何共同之處:

  • 它的長度是固定的,我們無法增加或減少它的長度。
  • 它正好佔用了內存中的那麼多空間。
  • 要訪問單個字節,需要另一個“視圖”對象,而不是 buffer[index]

ArrayBuffer 是一個內存區域。它裏面存儲了什麼?無從判斷。只是一個原始的字節序列。

2. 操作二進制數據

如要操作 ArrayBuffer,我們需要使用“視圖”對象。

2.1 視圖對象

視圖對象本身並不存儲任何東西。它是一副“眼鏡”,透過它來解釋存儲在 ArrayBuffer 中的字節。

例如:

  • Uint8Array —— 將 ArrayBuffer 中的每個字節視為 0 到 255 之間的單個數字(每個字節是 8 位,因此只能容納那麼多)。這稱為 “8 位無符號整數”。
  • Uint16Array —— 將每 2 個字節視為一個 0 到 65535 之間的整數。這稱為 “16 位無符號整數”。
  • Uint32Array —— 將每 4 個字節視為一個 0 到 4294967295 之間的整數。這稱為 “32 位無符號整數”。
  • Float64Array —— 將每 8 個字節視為一個 5.0x10-3241.8x10308 之間的浮點數。

因此,一個 16 字節 ArrayBuffer 中的二進制數據可以解釋為 16 個“小數字”,或 8 個更大的數字(每個數字 2 個字節),或 4 個更大的數字(每個數字 4 個字節),或 2 個高精度的浮點數(每個數字 8 個字節)。

2.2 使用視圖操作二進制數據

ArrayBuffer 是核心對象,是所有的基礎,是原始的二進制數據。

但是,如果我們要寫入值或遍歷它,基本上幾乎所有操作 —— 我們必須使用視圖(view),例如:

let buffer = new ArrayBuffer(16); // 創建一個長度為 16 的 buffer

let view = new Uint32Array(buffer); // 將 buffer 視為一個 32 位整數的序列

alert(Uint32Array.BYTES_PER_ELEMENT); // 每個整數 4 個字節

alert(view.length); // 4,它存儲了 4 個整數
alert(view.byteLength); // 16,字節中的大小

// 讓我們寫入一個值
view[0] = 123456;

// 遍歷值
for(let num of view) {
  alert(num); // 123456,然後 0,0,0(一共 4 個值)
}

2.3 TypedArray

所有這些視圖(Uint8ArrayUint32Array 等)的通用術語是 TypedArray。它們共享同一方法和屬性集。

請注意,沒有名為 TypedArray 的構造器,它只是表示 ArrayBuffer 上的視圖之一的通用總稱術語。Int8ArrayUint8Array 及其他,很快就會有完整列表。

當你看到 new TypedArray 之類的內容時,它表示 new Int8Arraynew Uint8Array 及其他中之一。

類型化數組的行為類似於常規數組:具有索引,並且是可迭代的。

一個類型化數組的構造器(無論是 Int8ArrayFloat64Array,都無關緊要),其行為各不相同,並且取決於參數類型。

參數有 5 種變體:

new TypedArray(buffer, [byteOffset], [length]);
new TypedArray(object);
new TypedArray(typedArray);
new TypedArray(length);
new TypedArray();`
  1. 如果給定的是 ArrayBuffer 參數,則會在其上創建視圖。我們已經用過該語法了。

    可選,我們可以給定起始位置 byteOffset(默認為 0)以及 length(默認至 buffer 的末尾),這樣視圖將僅涵蓋 buffer 的一部分。

  2. 如果給定的是 Array,或任何類數組對象,則會創建一個相同長度的類型化數組,並複製其內容。

    我們可以使用它來預填充數組的數據:

    let arr = new Uint8Array([0, 1, 2, 3]);
    alert( arr.length ); // 4,創建了相同長度的二進制數組
    alert( arr[1] ); // 1,用給定值填充了 4 個字節(無符號 8 位整數)`
  3. 如果給定的是另一個 TypedArray,也是如此:創建一個相同長度的類型化數組,並複製其內容。如果需要的話,數據在此過程中會被轉換為新的類型。

     let arr16 = new Uint16Array([1, 1000]);
     let arr8 = new Uint8Array(arr16);
     alert( arr8[0] ); // 1
     alert( arr8[1] ); // 232,試圖複製 1000,但無法將 1000 放進 8 位字節中(詳述見下文)。
  4. 對於數字參數 length —— 創建類型化數組以包含這麼多元素。它的字節長度將是 length 乘以單個 TypedArray.BYTES_PER_ELEMENT 中的字節數:

    `let arr = new Uint16Array(4); // 為 4 個整數創建類型化數組
    alert( Uint16Array.BYTES_PER_ELEMENT ); // 每個整數 2 個字節
    alert( arr.byteLength ); // 8(字節中的大小)`

  5. 不帶參數的情況下,創建長度為零的類型化數組。

我們可以直接創建一個 TypedArray,而無需提及 ArrayBuffer。但是,視圖離不開底層的 ArrayBuffer,因此,除第一種情況(已提供 ArrayBuffer)外,其他所有情況都會自動創建 ArrayBuffer

如要訪問底層的 ArrayBuffer,那麼在 TypedArray 中有如下的屬性:

  • arr.buffer —— 引用 ArrayBuffer
  • arr.byteLength —— ArrayBuffer 的長度。

因此,我們總是可以從一個視圖轉到另一個視圖:

let arr8 = new Uint8Array([0, 1, 2, 3]);

// 同一數據的另一個視圖
let arr16 = new Uint16Array(arr8.buffer);

下面是類型化數組的列表:

  • Uint8ArrayUint16ArrayUint32Array —— 用於 8、16 和 32 位的整數。
  • Uint8ClampedArray —— 用於 8 位整數,在賦值時便“固定“其值(見下文)。
  • Int8ArrayInt16ArrayInt32Array —— 用於有符號整數(可以為負數)。
  • Float32ArrayFloat64Array —— 用於 32 位和 64 位的有符號浮點數。
沒有 int8 或類似的單值類型

請注意,儘管有類似 Int8Array 這樣的名稱,但 JavaScript 中並沒有像 int,或 int8 這樣的單值類型。
這是合乎邏輯的,因為 Int8Array 不是這些單值的數組,而是 ArrayBuffer 上的視圖。

2.4 越界行為

如果我們嘗試將越界值寫入類型化數組會出現什麼情況?不會報錯。但是多餘的位被切除。

例如,我們嘗試將 256 放入 Uint8Array。256 的二進制格式是 100000000(9 位),但 Uint8Array 每個值只有 8 位,因此可用範圍為 0 到 255。

對於更大的數字,僅存儲最右邊的(低位有效)8 位,其餘部分被切除:

因此結果是 0。

257 的二進制格式是 100000001(9 位),最右邊的 8 位會被存儲,因此數組中會有 1

換句話説,該數字對 28 取模的結果被保存了下來。示例如下

let uint8array = new Uint8Array(16);

let num = 256;
alert(num.toString(2)); // 100000000(二進制表示)

uint8array[0] = 256;
uint8array[1] = 257;

alert(uint8array[0]); // 0
alert(uint8array[1]); // 1

Uint8ClampedArray 在這方面比較特殊,它的表現不太一樣。對於大於 255 的任何數字,它將保存為 255,對於任何負數,它將保存為 0。此行為對於圖像處理很有用。

3. TypedArray 方法

TypedArray 具有常規的 Array 方法,但有個明顯的例外。

我們可以遍歷(iterate),mapslicefindreduce 等。

但有幾件事我們做不了:

  • 沒有 splice —— 我們無法“刪除”一個值,因為類型化數組是緩衝區(buffer)上的視圖,並且緩衝區(buffer)是固定的、連續的內存區域。我們所能做的就是分配一個零值。
  • concat 方法。

還有兩種其他方法:

  • arr.set(fromArr, [offset])offset(默認為 0)開始,將 fromArr 中的所有元素複製到 arr
  • arr.subarray([begin, end]) 創建一個從 beginend(不包括)相同類型的新視圖。這類似於 slice 方法(同樣也支持),但不復制任何內容 —— 只是創建一個新視圖,以對給定片段的數據進行操作。

有了這些方法,我們可以複製、混合類型化數組,從現有數組創建新數組等。

4. DataView

DataView 是在 ArrayBuffer 上的一種特殊的超靈活“未類型化”視圖。它允許以任何格式訪問任何偏移量(offset)的數據。

  • 對於類型化的數組,構造器決定了其格式。整個數組應該是統一的。第 i 個數字是 arr[i]
  • 通過 DataView,我們可以使用 .getUint8(i).getUint16(i) 之類的方法訪問數據。我們在調用方法時選擇格式,而不是在構造的時候。

語法:

new DataView(buffer, [byteOffset], [byteLength])

  • buffer —— 底層的 ArrayBuffer。與類型化數組不同,DataView 不會自行創建緩衝區(buffer)。我們需要事先準備好。
  • byteOffset —— 視圖的起始字節位置(默認為 0)。
  • byteLength —— 視圖的字節長度(默認至 buffer 的末尾)。

例如,這裏我們從同一個 buffer 中提取不同格式的數字:

// 4 個字節的二進制數組,每個都是最大值 255
let buffer = new Uint8Array([255, 255, 255, 255]).buffer;

let dataView = new DataView(buffer);

// 在偏移量為 0 處獲取 8 位數字
alert( dataView.getUint8(0) ); // 255

// 現在在偏移量為 0 處獲取 16 位數字,它由 2 個字節組成,一起解析為 65535
alert( dataView.getUint16(0) ); // 65535(最大的 16 位無符號整數)

// 在偏移量為 0 處獲取 32 位數字
alert( dataView.getUint32(0) ); // 4294967295(最大的 32 位無符號整數)

dataView.setUint32(0, 0); // 將 4 個字節的數字設為 0,即將所有字節都設為 0`

當我們將混合格式的數據存儲在同一緩衝區(buffer)中時,DataView 非常有用。例如,當我們存儲一個成對序列(16 位整數,32 位浮點數)時,用 DataView 可以輕鬆訪問它們。

2. 總結

ArrayBuffer 是核心對象,是對固定長度的連續內存區域的引用。

幾乎任何對 ArrayBuffer 的操作,都需要一個視圖。

  1. 它可以是 TypedArray

    • Uint8ArrayUint16ArrayUint32Array —— 用於 8 位、16 位和 32 位無符號整數。
    • Int8ArrayInt16ArrayInt32Array —— 用於有符號整數(可以為負數)。
    • Float32ArrayFloat64Array —— 用於 32 位和 64 位的有符號浮點數。
  2. DataView —— 使用方法來指定格式的視圖,例如,getUint8(offset)

在大多數情況下,我們直接對類型化數組進行創建和操作,而將 ArrayBuffer 作為“共同之處(common denominator)”隱藏起來。我們可以通過 .buffer 來訪問它,並在需要時創建另一個視圖。

還有另外兩個術語,用於對二進制數據進行操作的方法的描述:

  • ArrayBufferView 是所有這些視圖的總稱。
  • BufferSourceArrayBufferArrayBufferView 的總稱。

我們將在下一章中學習這些術語。BufferSource 是最常用的術語之一,因為它的意思是“任何類型的二進制數據” —— ArrayBuffer 或其上的視圖。

這是一份備忘單:

user avatar guizimo 頭像 jidongdehai_co4lxh 頭像 yaofly 頭像 ziyeliufeng 頭像 hightopo 頭像 codepencil 頭像 shaochuancs 頭像 sunhengzhe 頭像 suporka 頭像 ailim 頭像 gaoming13 頭像 mulander 頭像
26 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.