一、 ArrayBuffer
ArrayBuffer對象、TypedArray視圖和DataView視圖是 JavaScript 操作二進制數據的一個接口。這些對象早就存在,屬於獨立的規格(2011 年 2 月發布),ES6 將它們納入了 ECMAScript 規格,並且增加了新的方法。它們都是以數組的語法處理二進制數據,所以統稱為二進制數組。
二進制數組由三類對象組成。
(1)ArrayBuffer對象:代表內存之中的一段二進制數據,可以通過“視圖”進行操作。“視圖”部署了數組接口,這意味着,可以用數組的方法操作內存。
(2)TypedArray視圖:共包括 9 種類型的視圖,比如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,就是大端字節序。
