JavaScript ArrayBuffer 二進位制陣列(一)
一、 ArrayBuffer
ArrayBuffer
物件、TypedArray
檢視和DataView
檢視是 JavaScript 操作二進位制資料的一個介面。這些物件早就存在,屬於獨立的規格(2011 年 2 月釋出),ES6 將它們納入了 ECMAScript 規格,並且增加了新的方法。它們都是以陣列的語法處理二進位制資料,所以統稱為二進位制陣列。
二進位制陣列由三類物件組成。
(1)ArrayBuffer
物件:代表記憶體之中的一段二進位制資料,可以通過“檢視”進行操作。“檢視”部署了陣列介面,這意味著,可以用陣列的方法操作記憶體。
(2)TypedArray
Uint8Array
(無符號 8 位整數)陣列檢視,Int16Array
(16 位整數)陣列檢視,Float32Array
(32 位浮點數)陣列檢視等等。
(3)DataView
檢視:可以自定義複合格式的檢視,比如第一個位元組是 Uint8(無符號 8 位整數)、第二、三個位元組是 Int16(16 位整數)、第四個位元組開始是 Float32(32 位浮點數)等等,此外還可以自定義位元組序。
簡單說,ArrayBuffer
物件代表原始的二進位制資料,TypedArray
檢視用來讀寫簡單型別的二進位制資料,DataView
檢視用來讀寫複雜型別的二進位制資料。
TypedArray
檢視支援的資料型別一共有 9 種(DataView
檢視支援除Uint8C
以外的其他 8 種)。
資料型別 | 位元組長度 | 含義 | 對應的 C 語言型別 |
---|---|---|---|
Int8 | 1 | 8 位帶符號整數 | signed char |
Uint8 | 1 | 8 位不帶符號整數 | unsigned char |
Uint8C | 1 | 8 位不帶符號整數(自動過濾溢位) | unsigned char |
Int16 | 2 | 16 位帶符號整數 | short |
Uint16 | 2 | 16 位不帶符號整數 | unsigned short |
Int32 | 4 | 32 位帶符號整數 | int |
Uint32 | 4 | 32 位不帶符號的整數 | unsigned int |
Float32 | 4 | 32 位浮點數 | float |
Float64 | 8 | 64 位浮點數 | double |
注意,二進位制陣列並不是真正的陣列,而是類似陣列的物件。
很多瀏覽器操作的 API,用到了二進位制陣列操作二進位制資料,下面是其中的幾個。
ArrayBuffer 物件概述
ArrayBuffer
物件代表儲存二進位制資料的一段記憶體,它不能直接讀寫,只能通過檢視(TypedArray
檢視和DataView
檢視)來讀寫,檢視的作用是以指定格式解讀二進位制資料。
ArrayBuffer
也是一個建構函式,可以分配一段可以存放資料的連續記憶體區域。
const buf = new ArrayBuffer(32);
上面程式碼生成了一段 32 位元組的記憶體區域,每個位元組的值預設都是 0。可以看到,ArrayBuffer
建構函式的引數是所需要的記憶體大小(單位位元組)。
為了讀寫這段內容,需要為它指定檢視。DataView
檢視的建立,需要提供ArrayBuffer
物件例項作為引數。
const buf = new ArrayBuffer(32);
const dataView = new DataView(buf);
dataView.getUint8(0) // 0
上面程式碼對一段 32 位元組的記憶體,建立DataView
檢視,然後以不帶符號的 8 位整數格式,從頭讀取 8 位二進位制資料,結果得到 0,因為原始記憶體的ArrayBuffer
物件,預設所有位都是 0。
另一種TypedArray
檢視,與DataView
檢視的一個區別是,它不是一個建構函式,而是一組建構函式,代表不同的資料格式。
const buffer = new ArrayBuffer(12);
const x1 = new Int32Array(buffer);
x1[0] = 1;
const x2 = new Uint8Array(buffer);
x2[0] = 2;
x1[0] // 2
上面程式碼對同一段記憶體,分別建立兩種檢視:32 位帶符號整數(Int32Array
建構函式)和 8 位不帶符號整數(Uint8Array
建構函式)。由於兩個檢視對應的是同一段記憶體,一個檢視修改底層記憶體,會影響到另一個檢視。
TypedArray
檢視的建構函式,除了接受ArrayBuffer
例項作為引數,還可以接受普通陣列作為引數,直接分配記憶體生成底層的ArrayBuffer
例項,並同時完成對這段記憶體的賦值。
const typedArray = new Uint8Array([0,1,2]);
typedArray.length // 3
typedArray[0] = 5;
typedArray // [5, 1, 2]
上面程式碼使用TypedArray
檢視的Uint8Array
建構函式,新建一個不帶符號的 8 位整數檢視。可以看到,Uint8Array
直接使用普通陣列作為引數,對底層記憶體的賦值同時完成。
ArrayBuffer.prototype.byteLength :返回所分配的記憶體區域的位元組長度
ArrayBuffer.prototype.slice() :允許將記憶體區域的一部分,拷貝生成一個新的ArrayBuffer
物件。
ArrayBuffer.isView()
ArrayBuffer
有一個靜態方法isView
,返回一個布林值,表示引數是否為ArrayBuffer
的檢視例項。這個方法大致相當於判斷引數,是否為TypedArray
例項或DataView
例項。
const buffer = new ArrayBuffer(8); ArrayBuffer.isView(buffer) // false const v = new Int32Array(buffer); ArrayBuffer.isView(v) // true
二、TypedArray 檢視
ArrayBuffer
物件作為記憶體區域,可以存放多種型別的資料。同一段記憶體,不同資料有不同的解讀方式,這就叫做“檢視”(view)。ArrayBuffer
有兩種檢視,一種是TypedArray
檢視,另一種是DataView
檢視。前者的陣列成員都是同一個資料型別,後者的陣列成員可以是不同的資料型別。
目前,TypedArray
檢視一共包括 9 種類型,每一種檢視都是一種建構函式。
Int8Array
:8 位有符號整數,長度 1 個位元組。Uint8Array
:8 位無符號整數,長度 1 個位元組。Uint8ClampedArray
:8 位無符號整數,長度 1 個位元組,溢位處理不同。Int16Array
:16 位有符號整數,長度 2 個位元組。Uint16Array
:16 位無符號整數,長度 2 個位元組。Int32Array
:32 位有符號整數,長度 4 個位元組。Uint32Array
:32 位無符號整數,長度 4 個位元組。Float32Array
:32 位浮點數,長度 4 個位元組。Float64Array
:64 位浮點數,長度 8 個位元組。
這 9 個建構函式生成的陣列,統稱為TypedArray
檢視。它們很像普通陣列,都有length
屬性,都能用方括號運算子([]
)獲取單個元素,所有陣列的方法,在它們上面都能使用。普通陣列與 TypedArray 陣列的差異主要在以下方面。
- TypedArray 陣列的所有成員,都是同一種類型。
- TypedArray 陣列的成員是連續的,不會有空位。
- TypedArray 陣列成員的預設值為 0。比如,
new Array(10)
返回一個普通陣列,裡面沒有任何成員,只是 10 個空位;new Uint8Array(10)
返回一個 TypedArray 陣列,裡面 10 個成員都是 0。 - TypedArray 陣列只是一層檢視,本身不儲存資料,它的資料都儲存在底層的
ArrayBuffer
物件之中,要獲取底層物件必須使用buffer
屬性。
// 建立一個8位元組的ArrayBuffer const b = new ArrayBuffer(8); // 建立一個指向b的Int32檢視,開始於位元組0,直到緩衝區的末尾 const v1 = new Int32Array(b); // 建立一個指向b的Uint8檢視,開始於位元組2,直到緩衝區的末尾 const v2 = new Uint8Array(b, 2); // 建立一個指向b的Int16檢視,開始於位元組2,長度為2 const v3 = new Int16Array(b, 2, 2);
TypedArray.prototype.copyWithin(target, start[, end = this.length])
TypedArray.prototype.entries()
TypedArray.prototype.every(callbackfn, thisArg?)
TypedArray.prototype.fill(value, start=0, end=this.length)
TypedArray.prototype.filter(callbackfn, thisArg?)
TypedArray.prototype.find(predicate, thisArg?)
TypedArray.prototype.findIndex(predicate, thisArg?)
TypedArray.prototype.forEach(callbackfn, thisArg?)
TypedArray.prototype.indexOf(searchElement, fromIndex=0)
TypedArray.prototype.join(separator)
TypedArray.prototype.keys()
TypedArray.prototype.lastIndexOf(searchElement, fromIndex?)
TypedArray.prototype.map(callbackfn, thisArg?)
TypedArray.prototype.reduce(callbackfn, initialValue?)
TypedArray.prototype.reduceRight(callbackfn, initialValue?)
TypedArray.prototype.reverse()
TypedArray.prototype.slice(start=0, end=this.length)
TypedArray.prototype.some(callbackfn, thisArg?)
TypedArray.prototype.sort(comparefn)
TypedArray.prototype.toLocaleString(reserved1?, reserved2?)
TypedArray.prototype.toString()
TypedArray.prototype.values()
BYTES_PER_ELEMENT 屬性
每一種檢視的建構函式,都有一個BYTES_PER_ELEMENT
屬性,表示這種資料型別佔據的位元組數。
Int8Array.BYTES_PER_ELEMENT // 1
Uint8Array.BYTES_PER_ELEMENT // 1
Uint8ClampedArray.BYTES_PER_ELEMENT // 1
Int16Array.BYTES_PER_ELEMENT // 2
Uint16Array.BYTES_PER_ELEMENT // 2
Int32Array.BYTES_PER_ELEMENT // 4
Uint32Array.BYTES_PER_ELEMENT // 4
Float32Array.BYTES_PER_ELEMENT // 4
Float64Array.BYTES_PER_ELEMENT // 8
三、DataView 檢視
如果一段資料包括多種型別(比如伺服器傳來的 HTTP 資料),這時除了建立ArrayBuffer
物件的複合檢視以外,還可以通過DataView
檢視進行操作。
DataView
檢視提供更多操作選項,而且支援設定位元組序。本來,在設計目的上,ArrayBuffer
物件的各種TypedArray
檢視,是用來向網絡卡、音效卡之類的本機裝置傳送資料,所以使用本機的位元組序就可以了;而DataView
檢視的設計目的,是用來處理網路裝置傳來的資料,所以大端位元組序或小端位元組序是可以自行設定的。
DataView
檢視本身也是建構函式,接受一個ArrayBuffer
物件作為引數,生成檢視。
new DataView(ArrayBuffer buffer [, 位元組起始位置 [, 長度]]);
下面是一個例子。
const buffer = new ArrayBuffer(24);
const dv = new DataView(buffer);
DataView
例項有以下屬性,含義與TypedArray
例項的同名方法相同。
DataView.prototype.buffer
:返回對應的 ArrayBuffer 物件DataView.prototype.byteLength
:返回佔據的記憶體位元組長度DataView.prototype.byteOffset
:返回當前檢視從對應的 ArrayBuffer 物件的哪個位元組開始
DataView
例項提供 8 個方法讀取記憶體。
getInt8
:讀取 1 個位元組,返回一個 8 位整數。getUint8
:讀取 1 個位元組,返回一個無符號的 8 位整數。getInt16
:讀取 2 個位元組,返回一個 16 位整數。getUint16
:讀取 2 個位元組,返回一個無符號的 16 位整數。getInt32
:讀取 4 個位元組,返回一個 32 位整數。getUint32
:讀取 4 個位元組,返回一個無符號的 32 位整數。getFloat32
:讀取 4 個位元組,返回一個 32 位浮點數。getFloat64
:讀取 8 個位元組,返回一個 64 位浮點數。
這一系列get
方法的引數都是一個位元組序號(不能是負數,否則會報錯),表示從哪個位元組開始讀取。
const buffer = new ArrayBuffer(24);
const dv = new DataView(buffer);
// 從第1個位元組讀取一個8位無符號整數
const v1 = dv.getUint8(0);
// 從第2個位元組讀取一個16位無符號整數
const v2 = dv.getUint16(1);
// 從第4個位元組讀取一個16位無符號整數
const v3 = dv.getUint16(3);
上面程式碼讀取了ArrayBuffer
物件的前 5 個位元組,其中有一個 8 位整數和兩個十六位整數。
如果一次讀取兩個或兩個以上位元組,就必須明確資料的儲存方式,到底是小端位元組序還是大端位元組序。預設情況下,DataView
的get
方法使用大端位元組序解讀資料,如果需要使用小端位元組序解讀,必須在get
方法的第二個引數指定true
。
// 小端位元組序
const v1 = dv.getUint16(1, true);
// 大端位元組序
const v2 = dv.getUint16(3, false);
// 大端位元組序
const v3 = dv.getUint16(3);
DataView 檢視提供 8 個方法寫入記憶體。
setInt8
:寫入 1 個位元組的 8 位整數。setUint8
:寫入 1 個位元組的 8 位無符號整數。setInt16
:寫入 2 個位元組的 16 位整數。setUint16
:寫入 2 個位元組的 16 位無符號整數。setInt32
:寫入 4 個位元組的 32 位整數。setUint32
:寫入 4 個位元組的 32 位無符號整數。setFloat32
:寫入 4 個位元組的 32 位浮點數。setFloat64
:寫入 8 個位元組的 64 位浮點數。
這一系列set
方法,接受兩個引數,第一個引數是位元組序號,表示從哪個位元組開始寫入,第二個引數為寫入的資料。對於那些寫入兩個或兩個以上位元組的方法,需要指定第三個引數,false
或者undefined
表示使用大端位元組序寫入,true
表示使用小端位元組序寫入。
// 在第1個位元組,以大端位元組序寫入值為25的32位整數
dv.setInt32(0, 25, false);
// 在第5個位元組,以大端位元組序寫入值為25的32位整數
dv.setInt32(4, 25);
// 在第9個位元組,以小端位元組序寫入值為2.5的32位浮點數
dv.setFloat32(8, 2.5, true);
如果不確定正在使用的計算機的位元組序,可以採用下面的判斷方式。
const littleEndian = (function() {
const buffer = new ArrayBuffer(2);
new DataView(buffer).setInt16(0, 256, true);
return new Int16Array(buffer)[0] === 256;
})();
如果返回true
,就是小端位元組序;如果返回false
,就是大端位元組序。