在 Web 開發中,當我們處理文件時(創建,上傳,下載),經常會遇到二進制數據。另一個典型的應用場景是圖像處理。
與其他語言相比,JavaScript 中的二進制數據是以非標準方式實現的。
1. 創建二進制數據
基本的二進制對象是 ArrayBuffer —— 對固定長度的連續內存空間的引用。
let buffer = new ArrayBuffer(16); // 創建一個長度為 16 的 buffer
alert(buffer.byteLength); // 16
它會分配一個 16 字節的連續內存空間,並用 0 進行預填充
注意:
ArrayBuffer不是某種東西的數組
讓我們先澄清一個可能的誤區。ArrayBuffer與Array沒有任何共同之處:
- 它的長度是固定的,我們無法增加或減少它的長度。
- 它正好佔用了內存中的那麼多空間。
- 要訪問單個字節,需要另一個“視圖”對象,而不是
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-324到1.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
所有這些視圖(Uint8Array,Uint32Array 等)的通用術語是 TypedArray。它們共享同一方法和屬性集。
請注意,沒有名為 TypedArray 的構造器,它只是表示 ArrayBuffer 上的視圖之一的通用總稱術語。Int8Array,Uint8Array 及其他,很快就會有完整列表。
當你看到 new TypedArray 之類的內容時,它表示 new Int8Array、new Uint8Array 及其他中之一。
類型化數組的行為類似於常規數組:具有索引,並且是可迭代的。
一個類型化數組的構造器(無論是 Int8Array 或 Float64Array,都無關緊要),其行為各不相同,並且取決於參數類型。
參數有 5 種變體:
new TypedArray(buffer, [byteOffset], [length]);
new TypedArray(object);
new TypedArray(typedArray);
new TypedArray(length);
new TypedArray();`
-
如果給定的是
ArrayBuffer參數,則會在其上創建視圖。我們已經用過該語法了。可選,我們可以給定起始位置
byteOffset(默認為 0)以及length(默認至 buffer 的末尾),這樣視圖將僅涵蓋buffer的一部分。 -
如果給定的是
Array,或任何類數組對象,則會創建一個相同長度的類型化數組,並複製其內容。我們可以使用它來預填充數組的數據:
let arr = new Uint8Array([0, 1, 2, 3]); alert( arr.length ); // 4,創建了相同長度的二進制數組 alert( arr[1] ); // 1,用給定值填充了 4 個字節(無符號 8 位整數)` -
如果給定的是另一個
TypedArray,也是如此:創建一個相同長度的類型化數組,並複製其內容。如果需要的話,數據在此過程中會被轉換為新的類型。let arr16 = new Uint16Array([1, 1000]); let arr8 = new Uint8Array(arr16); alert( arr8[0] ); // 1 alert( arr8[1] ); // 232,試圖複製 1000,但無法將 1000 放進 8 位字節中(詳述見下文)。 -
對於數字參數
length—— 創建類型化數組以包含這麼多元素。它的字節長度將是length乘以單個TypedArray.BYTES_PER_ELEMENT中的字節數:`let arr = new Uint16Array(4); // 為 4 個整數創建類型化數組
alert( Uint16Array.BYTES_PER_ELEMENT ); // 每個整數 2 個字節
alert( arr.byteLength ); // 8(字節中的大小)` - 不帶參數的情況下,創建長度為零的類型化數組。
我們可以直接創建一個 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);
下面是類型化數組的列表:
Uint8Array,Uint16Array,Uint32Array—— 用於 8、16 和 32 位的整數。Uint8ClampedArray—— 用於 8 位整數,在賦值時便“固定“其值(見下文)。Int8Array,Int16Array,Int32Array—— 用於有符號整數(可以為負數)。Float32Array,Float64Array—— 用於 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),map,slice,find 和 reduce 等。
但有幾件事我們做不了:
- 沒有
splice—— 我們無法“刪除”一個值,因為類型化數組是緩衝區(buffer)上的視圖,並且緩衝區(buffer)是固定的、連續的內存區域。我們所能做的就是分配一個零值。 - 無
concat方法。
還有兩種其他方法:
arr.set(fromArr, [offset])從offset(默認為 0)開始,將fromArr中的所有元素複製到arr。arr.subarray([begin, end])創建一個從begin到end(不包括)相同類型的新視圖。這類似於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 的操作,都需要一個視圖。
-
它可以是
TypedArray:Uint8Array,Uint16Array,Uint32Array—— 用於 8 位、16 位和 32 位無符號整數。Int8Array,Int16Array,Int32Array—— 用於有符號整數(可以為負數)。Float32Array,Float64Array—— 用於 32 位和 64 位的有符號浮點數。
- 或
DataView—— 使用方法來指定格式的視圖,例如,getUint8(offset)。
在大多數情況下,我們直接對類型化數組進行創建和操作,而將 ArrayBuffer 作為“共同之處(common denominator)”隱藏起來。我們可以通過 .buffer 來訪問它,並在需要時創建另一個視圖。
還有另外兩個術語,用於對二進制數據進行操作的方法的描述:
ArrayBufferView是所有這些視圖的總稱。BufferSource是ArrayBuffer或ArrayBufferView的總稱。
我們將在下一章中學習這些術語。BufferSource 是最常用的術語之一,因為它的意思是“任何類型的二進制數據” —— ArrayBuffer 或其上的視圖。
這是一份備忘單: