在網頁開發中,我們大多在處理檔案(建立、上傳、下載)時會遇到二進制資料。另一個典型的使用案例是影像處理。
JavaScript 中都可以做到,而且二進制運算效能很高。
不過,由於類別很多,因此會有些混淆。舉例來說
ArrayBuffer
、Uint8Array
、DataView
、Blob
、File
等。
與其他語言相比,JavaScript 中的二進制資料是以非標準的方式實作的。但是,當我們釐清後,一切都變得相當簡單。
基本的二進制物件是 ArrayBuffer
,它是一個指向固定長度連續記憶體區域的參考。
我們這樣建立它
let buffer = new ArrayBuffer(16); // create a buffer of length 16
alert(buffer.byteLength); // 16
這會配置一個 16 位元組的連續記憶體區域,並預先填入零。
ArrayBuffer
不是某個陣列讓我們消除一個可能的混淆來源。ArrayBuffer
與 Array
沒有任何共通點
- 它有一個固定的長度,我們無法增加或減少它。
- 它在記憶體中佔用的空間大小剛好就是這麼多。
- 若要存取個別位元組,需要另一個「檢視」物件,而不是
buffer[index]
。
ArrayBuffer
是個記憶體區域。裡面儲存了什麼?它不知道。只是一個位元組的原始序列。
若要操作 ArrayBuffer
,我們需要使用「檢視」物件。
檢視物件本身不儲存任何東西。它是「眼鏡」,提供對儲存在 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 個位元組)。
ArrayBuffer
是核心物件,所有事物的根源,原始的二進位資料。
但是,如果我們要寫入它,或遍歷它,基本上對於幾乎任何操作,我們都必須使用檢視,例如
let buffer = new ArrayBuffer(16); // create a buffer of length 16
let view = new Uint32Array(buffer); // treat buffer as a sequence of 32-bit integers
alert(Uint32Array.BYTES_PER_ELEMENT); // 4 bytes per integer
alert(view.length); // 4, it stores that many integers
alert(view.byteLength); // 16, the size in bytes
// let's write a value
view[0] = 123456;
// iterate over values
for(let num of view) {
alert(num); // 123456, then 0, 0, 0 (4 values total)
}
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
的一部分。 -
如果提供
Array
或任何類似陣列的物件,則會建立長度相同的類型化陣列並複製內容。我們可以使用它來預先填入資料陣列
let arr = new Uint8Array([0, 1, 2, 3]); alert( arr.length ); // 4, created binary array of the same length alert( arr[1] ); // 1, filled with 4 bytes (unsigned 8-bit integers) with given values
-
如果提供另一個
TypedArray
,則會執行相同的動作:建立長度相同的類型化陣列並複製值。如有需要,值會在過程中轉換為新的類型。let arr16 = new Uint16Array([1, 1000]); let arr8 = new Uint8Array(arr16); alert( arr8[0] ); // 1 alert( arr8[1] ); // 232, tried to copy 1000, but can't fit 1000 into 8 bits (explanations below)
-
對於數字引數
length
,會建立包含這麼多元素的類型化陣列。其位元組長度會是length
乘以單一項目中的位元組數目TypedArray.BYTES_PER_ELEMENT
let arr = new Uint16Array(4); // create typed array for 4 integers alert( Uint16Array.BYTES_PER_ELEMENT ); // 2 bytes per integer alert( arr.byteLength ); // 8 (size in bytes)
-
如果沒有引數,則會建立長度為零的類型化陣列。
我們可以直接建立 TypedArray
,而不用提到 ArrayBuffer
。但檢視無法在沒有底層 ArrayBuffer
的情況下存在,因此在所有這些情況下都會自動建立,除了第一個情況(已提供時)。
若要存取底層 ArrayBuffer
,TypedArray
中有下列屬性
buffer
– 參照ArrayBuffer
。byteLength
–ArrayBuffer
的長度。
因此,我們可以隨時從一個檢視移動到另一個檢視
let arr8 = new Uint8Array([0, 1, 2, 3]);
// another view on the same data
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
上的檢視。
超出界線的行為
如果我們嘗試將超出界線的值寫入類型化陣列中,會發生什麼事?不會有錯誤。但額外的位元會被切斷。
例如,我們嘗試將 256 放入 Uint8Array
中。在二進位形式中,256 是 100000000
(9 位元),但 Uint8Array
每個值僅提供 8 位元,這使得可用範圍從 0 到 255。
對於較大的數字,只有最右邊(重要性較低)的 8 位元會被儲存,其餘的會被切斷
因此,我們將得到零。
對於 257,二進位形式是 100000001
(9 位元),最右邊的 8 位元會被儲存,因此我們在陣列中會有 1
換句話說,數字模 28 會被儲存。
以下是範例
let uint8array = new Uint8Array(16);
let num = 256;
alert(num.toString(2)); // 100000000 (binary representation)
uint8array[0] = 256;
uint8array[1] = 257;
alert(uint8array[0]); // 0
alert(uint8array[1]); // 1
Uint8ClampedArray
在這方面很特別,它的行為不同。它會將大於 255 的任何數字儲存為 255,將任何負數儲存為 0。這種行為對於影像處理很有用。
類型化陣列方法
TypedArray
具有常規 Array
方法,但有顯著的例外。
我們可以進行反覆運算、map
、slice
、find
、reduce
等。
不過,有幾件事我們無法做
- 沒有
splice
– 我們無法「刪除」一個值,因為類型化陣列是緩衝區的檢視,而這些是固定的、連續的記憶體區域。我們能做的就是指定一個零。 - 沒有
concat
方法。
有兩個額外的
arr.set(fromArr, [offset])
將fromArr
中的所有元素複製到arr
中,從位置offset
開始(預設為 0)。arr.subarray([begin, end])
從begin
到end
(不包含)建立同類型的新檢視。這類似於slice
方法(也受支援),但不會複製任何內容 – 只建立一個新檢視,以操作給定的資料部分。
這些方法允許我們複製類型化陣列、將它們混合、從現有陣列建立新陣列,等等。
DataView
DataView 是一個特殊的超靈活「非類型化」檢視,用於 ArrayBuffer
。它允許以任何格式存取任何偏移量中的資料。
- 對於類型化陣列,建構函式會決定格式為何。整個陣列應該是一致的。第 i 個數字是
arr[i]
。 - 使用
DataView
,我們使用.getUint8(i)
或.getUint16(i)
等方法存取資料。我們在方法呼叫時間而不是建構時間選擇格式。
語法
new DataView(buffer, [byteOffset], [byteLength])
buffer
– 底層ArrayBuffer
。與類型化陣列不同,DataView
不會自行建立緩衝區。我們需要準備好它。byteOffset
– 檢視的起始位元組位置(預設為 0)。byteLength
– 檢視的位元組長度(預設為buffer
的尾端)。
例如,這裡我們從同一個緩衝區中以不同的格式提取數字
// binary array of 4 bytes, all have the maximal value 255
let buffer = new Uint8Array([255, 255, 255, 255]).buffer;
let dataView = new DataView(buffer);
// get 8-bit number at offset 0
alert( dataView.getUint8(0) ); // 255
// now get 16-bit number at offset 0, it consists of 2 bytes, together interpreted as 65535
alert( dataView.getUint16(0) ); // 65535 (biggest 16-bit unsigned int)
// get 32-bit number at offset 0
alert( dataView.getUint32(0) ); // 4294967295 (biggest 32-bit unsigned int)
dataView.setUint32(0, 0); // set 4-byte number to zero, thus setting all bytes to 0
當我們將混合格式資料儲存在同一個緩衝區時,DataView
非常有用。例如,當我們儲存一系列的配對(16 位元整數、32 位元浮點數)時,DataView
可以讓我們輕鬆地存取它們。
摘要
ArrayBuffer
是核心物件,它是一個指向固定長度連續記憶體區域的參考。
為了對 ArrayBuffer
執行幾乎任何操作,我們需要一個檢視。
- 它可以是
TypedArray
Uint8Array
、Uint16Array
、Uint32Array
– 分別用於 8、16 和 32 位元的無符號整數。Uint8ClampedArray
– 用於 8 位元整數,在指定時會將它們「固定」。Int8Array
、Int16Array
、Int32Array
– 針對有符號整數(可以為負數)。Float32Array
、Float64Array
– 針對 32 和 64 位元的浮點數。
- 或
DataView
– 使用方法來指定格式的檢視,例如getUint8(offset)
。
在多數情況下,我們會直接建立和操作型別化陣列,讓 ArrayBuffer
隱藏起來,作為「公分母」。我們可以將其存取為 .buffer
,並在需要時建立另一個檢視。
在描述操作二進位資料的方法時,還有兩個額外的術語
ArrayBufferView
是所有這些檢視類型的統稱。BufferSource
是ArrayBuffer
或ArrayBufferView
的統稱。
我們將在後續章節中看到這些術語。BufferSource
是最常見的術語之一,因為它表示「任何類型的二進位資料」– ArrayBuffer
或其上的檢視。
以下是作弊清單
留言
<code>
標籤,若要插入多行程式碼,請將其包覆在<pre>
標籤中,若要插入超過 10 行程式碼,請使用沙盒 (plnkr、jsbin、codepen…)