天天看點

Threejs 拓展之二進制數組

在Threejs 的學習過程中,配置設定緩存區域時需要調用JavaScript中的Uint16Array、Float32Array等對象來配置設定連續的記憶體空間。看到Uint16Array、Float32Array時,感覺之前學了假的JavaScript。查資料發現,《ES6标準入門 第二版》的第十二章二進制數組 詳細的介紹了上面的幾個對象。

二進制數組

二進制數組(ArrayBuffer 對象、TypedArray 視圖和 DataView 視圖)是JavaScript操作二進制資料的一個接口。這些對象早就存在,屬于獨立的規格(2011年2月釋出),ES6将它們納入ECMAScript規格,并增加了新的方法。

這個接口的原始設計目的,與WebGL項目有關。所謂WebGL,就是指浏覽器與顯示卡之間的通信接口,為了滿足JavaScript與顯示卡之間大量的、實時的資料交換,它們之間的資料通信必須是二進制的,而不能是傳統的文本格式。文本格式傳遞一個32位整數,兩端的JavaScript腳本與顯示卡都要進行格式轉化,将非常耗時。這時要是存在一種機制,類似C語言,直接操作位元組,将4個位元組的32位整數,以二進制形式原封不動地送入顯示卡,腳本的性能将會大幅提升。

二進制數組就是在這種背景下誕生的。它很像C語言的數組,允許開發者以數組下标的形式,直接操作記憶體,大大增強了JavaScript處理二進制資料的能力,使得開發者有可能通過JavaScript與作業系統的原生接口進行二進制通信。

二進制數組由三類對象組成:

1-- ArrayBuffer 對象:代表記憶體之中的一段二進制資料,可以通過“視圖”進行操作。“視圖”部署了數組接口,這也就是說,可以用數組的方法操作記憶體。

2-- TypedArray 視圖:共包括9種類型的視圖,比如Uint8Array數組視圖、Int16Array數組視圖、Float32Array數組視圖等。

3-- DataView 視圖:可以自定義複合格式的視圖,比如第一個位元組是Uint8,第二、三個位元組是Int6、第四個位元組是Float32等等,此外還可以自定義位元組序。

簡單說,ArrayBuffer 對象代表原始的二進制資料,TypedArray 視圖用來讀寫簡單類型的二進制資料,DataView 視圖用來讀寫複雜類型的二進制資料。

TypedArray 視圖支援的資料類型一共有9種,DataView 視圖支援除Uint8C以外的其他8種。

Threejs 拓展之二進制數組

注意:二進制數組并不是真正的數組,而是類似數組的對象。

很多浏覽器操作的API,用到了二進制數組操作二進制資料,比如:File API、XML HTTPRequest、Fetch API、Canvas、WebSockets。

ArrayBuffer 對象

ArrayBuffer 對象代表儲存二進制資料的一段記憶體,它不能直接讀寫,隻能通過視圖(TypedArray 視圖和 DataView 視圖)來讀寫,視圖的作用是以指定格式解讀二進制資料。

ArrayBuffer 也是一個構造函數,可以配置設定一段可以存放資料的連續記憶體區域。

上面代碼生成了一段32位元組的記憶體區域,每個位元組的值預設都是0。可以看到,ArrayBuffer 構造函數的參數是所需要的記憶體大小(機關位元組)。

為了讀寫這段内容,需要為它指定視圖。DataView 視圖的建立,需要提供ArrayBuffer 對象執行個體作為參數。

var buf = new ArrayBuffer(32);
var dataView = new DataView(buf);
dataView.getUint8(0);  //0
           

上面代碼對一段32位元組的記憶體,建立DataView視圖,然後以不帶符号的8位整數格式,讀取第一個元素,結果得到0,因為原始記憶體的ArrayBuffer 對象預設所有位都是0。

另一種 TypedArray 視圖,與 DataView 視圖的一個差別是,它不是一個構造函數,而是一組構造函數,代表不同的資料格式。

var buffer = new ArrayBuffer(12);

var x1 = new Int32Array(buffer);
x1[0] = 1;
var x2 = new Uint8Array(buffer);
x2[0] = 2;

console.log(x1[0]); //2
           

上面代碼對同一段記憶體,分别建立兩種視圖:32位帶符号整數(Int32Array 構造函數)和8位不帶符号整數(Uint8Array 構造函數)。由于兩個視圖對應的是同一段記憶體,一個視圖修改底層記憶體,會影響到另一個視圖。

TypedArray 視圖的構造函數,除了接受 ArrayBuffer 執行個體作為參數,還可以接受普通數組作為參數,直接配置設定記憶體生成底層的ArrayBuffer執行個體,并同時完成對這段記憶體的指派。

var typedArray = new Uint8Array([0,1,2]);
console.log(typedArray.length);  //3

typedArray[0] = 5;
console.log(typedArray);  //[5,1,2]
           

上面代碼使用TypedArray 視圖的Uint8Array 構造函數,建立一個不帶符号的8位整數視圖。可以看到,Uint8Array 直接使用普通數組作為參數,對底層記憶體的指派同時完成。

ArrayBuffer.prototype.byteLength

ArrayBuffer 執行個體的 byteLength 屬性,傳回所配置設定的記憶體區域的位元組長度。

var buffer = new ArrayBuffer(32);
console.log(buffer.byteLength);  //32
           

如果要配置設定的記憶體區域很大,有可能配置設定失敗(可能沒有那麼多的連續空餘記憶體),是以有必要檢查是否配置設定成功。

if(buffer.byteLength === n) {
  // 成功
} else {
  // 失敗
}
           

ArrayBuffer.prototype.slice()

ArrayBuffer 執行個體有一個 slice 方法,允許将記憶體區域的一部分,拷貝生成一個新的 ArrayBuffer 對象。

var buffer = new ArrayBuffer(8);
var newBuffer = buffer.slice(0, 3);
           

上面代碼拷貝 buffer 對象的前3個位元組(從0開始,到第3個位元組前結束),生成一個新的 ArrayBuffer 對象。slice 方法其實包括兩步,第一步是先配置設定一段記憶體,第二步是将原來那個 ArrayBuffer 對象拷貝進去。

slice 方法接受兩個參數,第一個參數表示拷貝開始的位元組序号(含該位元組),第二個參數表示拷貝截止的位元組序号(不含該位元組)。如果省略第二個參數,則預設到原 ArrayBuffer 對象的結尾。

除了slice 方法,ArrayBuffer 對象不提供任何直接讀寫記憶體的方法,隻允許在其上方建立視圖,然後通過視圖讀寫。

ArrayBuffer.isView()

ArrayBuffer 有一個靜态方法isView,傳回一個布爾值,表示參數是否為 ArrayBuffer的視圖執行個體。這個方法大緻相當于判斷參數,是否為 TypedArray 執行個體或 DataView 執行個體。

var buffer = new ArrayBuffer(8);
ArrayBuffer.isView(buffer);   //false

var 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 屬性。

構造函數

TypedArray 數組提供9種構造函數,用來生成相應類型的數組執行個體。

構造函數有多種方法:

1> TypedArray(buffer, byteOffset=0, length?)

同一個 ArrayBuffer 對象之上,可以根據不同的資料類型,建立多個視圖。

// 建立一個8位元組的ArrayBuffer
var b = new ArrayBuffer(8);

// 建立一個指向b的Int32視圖,開始于位元組0,直到緩沖區的末尾
var v1 = new Int32Array(b);

// 建立一個指向b的Uint8視圖,開始于位元組2,直到緩沖區的末尾
var v2 = new Uint8Array(b, 2);

// 建立一個指向b的Int16視圖,開始于位元組2,長度為2
var v3 = new Int16Array(b, 2, 2);
           

上面代碼在一段長度為8個位元組的記憶體(b)之上,生成了3個視圖:v1、v2、v3。

視圖的構造函數可以接受三個參數:

    · 第一參數 -- 必需:視圖對應的底層 ArrayBuffer 對象。

    · 第二參數 -- 可選:視圖開始的位元組序号,預設從0開始。

    · 第三參數 -- 可選:視圖包含的資料個數,預設直到本段記憶體區域結束。

So,v1、v2和v3是重疊的:v1[0]是一個32位整數,指向位元組0--位元組3;v2[0]是一個8位無符号整數,指向位元組2;v3[0]是一個16位整數,指向位元組2--位元組3。隻要任何一個視圖對記憶體有所修改,就會在另外兩個視圖上反映出來。

注意:byteOffset 必須與所要建立的資料類型一緻,否則會報錯。

var buffer = new ArrayBuffer(8);
var i16 = new Int16Array(buffer, 1);
//Uncaught RangeError: start offset of Int16Array should be a multiple of 2
           

上面代碼中,新生成一個8個位元組的 ArrayBuffer 對象,然後在這個對象的第一個位元組,建立帶符号的16位整數視圖,結果報錯。因為,帶符号的16位整數需要兩個位元組,是以 byteOffset 參數必須能夠被2整除。

如果想從任意位元組開始解讀 ArrayBuffer 對象,必須使用 DataView 視圖,因為 TypedArray 視圖隻提供9種固定的解讀格式。

2> TypedArray(length)

視圖還可以不通過 ArrayBuffer 對象,直接配置設定記憶體而生成。

var f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];   //30
           

上面代碼生成一個8個成員的 Float64Array 數組(共64個位元組),然後依次對每個成員指派。這時,視圖構造函數的參數就是成員的個數。可以看到,視圖數組的指派操作與普通數組的操作并無兩樣。

3> TypedArray(typedArray)

TypedArray 數組的構造函數,可以接受另一個TypedArray 執行個體作為參數:

上面代碼中,Int8Array 構造函數接受一個 Uint8Array 執行個體作為參數。

注意,此時生成的新數組,隻是複制了參數數組的值,對應的底層記憶體是不一樣的。新數組會開辟一段新的記憶體儲存資料,不會在原數組的記憶體之上建立視圖。

var x = new Int8Array([1, 1]);
var y = new Int8Array(x);
console.log(x[0]); //1
console.log(y[0]); //1

x[0] = 2;
console.log(y[0]); //1
           

上面代碼中,數組y是以數組x為模闆而生成的,當x變動的時候,y并沒有變動。

如果想基于同一段記憶體,構造不同的視圖,可以采用如下的方法。

var x = new Int8Array([1, 1]);
var y = new Int8Array(x.buffer);
console.log(x[0]);  //1
console.log(y[0]);  //1

x[0] = 2;
console.log(y[0]);  //2
           

4> TypedArray(arrayLikeObject)

構造函數的參數也可以是一個普通數組,然後直接生成 TypedArray 執行個體。

注意,這時 TypedArray 視圖會重新開辟記憶體,不會在原數組的記憶體上建立視圖。

上面代碼從一個普通的數組,生成一個8位無符号整數的 TypedArray 執行個體。

TypedArray 數組也可以轉換回普通數組。

數組方法

普通數組的操作方法和屬性,對 TypedArray 數組完全适用。

· 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.findInex( 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( )

注意,TypedArray 數組沒有 concat 方法。如果想要合并多個 TypedArray 數組,可以使用下面這個函數:

function concatenate(resultConstructor, ...arrays) {
	let totalLength = 0;
	for(let arr of arrays) {
		totalLength += arr.length;
	}
	let result = new resultConstructor(totalLength);
	let offset = 0;
	for(let arr of arrays) {
		result.set(arr, offset);
		offset += arr.length;
	}
	return result;
} 
concatenate(Uint8Array, Uint8Array.of(1,2), Uint8Array.of(3,4));
//Uint8Array[1,2,3,4]
           

另外,TypedArray 數組與普通數組一樣,部署了 Iterator 接口,是以可以被周遊。

let ui8 = Uint8Array.of(0,1,2);
for(let byte of ui8) {
	console.log(byte);// 0  1  2
}
           

位元組序

位元組序指的是數值在記憶體中的表示方式。

var buffer = new ArrayBuffer(16);
var int32View = new Int32Array(buffer);
for(var i=0;i<int32View.length;i++) {
	int32View[i] = i * 2;
}
           

上面代碼生成一個16位元組的 ArrayBuffer 對象,然後在它的基礎上,建立一個32位整數的視圖。由于每個32位整數占據4個位元組,所有可以寫入4個整數,依次為0,2,4,6。

如果在這段資料上接着建立一個16位整數的視圖,可以讀出完全不一樣的結果。

var int16View = new Int16Array(buffer);
for(var i=0;i<int16View.length;i++) {
	console.log("Entry " + i + ": " + int16View[i]);
}
// Entry 0: 0
// Entry 1: 0
// Entry 2: 2
// Entry 3: 0
// Entry 4: 4
// Entry 5: 0
// Entry 6: 6
// Entry 7: 0
           

由于每個16位整數占據2個位元組,是以整個 ArrayBuffer 對象現在分成8段。由于x86體系的計算機都采用 小端位元組序(little endian),相對 重要的位元組排在後面的記憶體位址,相對 不重要位元組排在前面的記憶體位址,是以就得到了上面的結果。

比如,一個占據四個位元組的16進制數0x12345678,決定其大小的最重要的位元組是“12”,最不重要的是“78”。小端位元組序将最不重要的位元組排在前面,儲存順序就是78563412;大端位元組序則完全相反,将最重要的位元組排在前面,儲存順序就是12345678。目前,所有個人電腦幾乎都是小端位元組序,是以 TypedArray 數組内部也采用小端位元組序讀寫資料,或者更準确的說,按照本機作業系統設定的位元組序讀寫資料。

這并不意味大端位元組序不重要,事實上,很多網絡裝置和特定的作業系統采用的是大端位元組序。這就帶來一個嚴重的問題:如果一段資料是大端位元組序,TypedArray 數組将無法正确解析,因為它隻能處理小端位元組序!為了解決這個問題,JavaScript 引入 DataView 對象,可以設定位元組序(後面介紹)。

例子:

// 假定某段 buffer 包含如下位元組 [0x02, 0x01, 0x03, 0x07]
var buffer = new ArrayBuffer(4);
var v1 = new Uint8Array(buffer);
v1[0] = 2;
v1[1] = 1;
v1[2] = 3;
v1[3] = 7;

var uInt16View = new Uint16Array(buffer);

// 計算機采用小端位元組序
// 是以頭兩個位元組等于258
if(uInt16View[0] === 258) {
	console.log('ok');   //ok
}

// 指派運算
uInt16View[0] = 255;     // 位元組變為 [0xFF, 0x00, 0x03, 0x07]
uInt16View[0] = 0xff05;  // 位元組變為 [0x05, 0xFF, 0x03, 0x07]
uInt16View[1] = 0x0210;  // 位元組變為 [0x05, 0xFF, 0x10, 0x02] 
           

下面的函數可以用來判斷 目前視圖是小端位元組序還是大端位元組序。

const BIG_ENDIAN = Symbol('BIG_ENDIAN');
const LITTLE_ENDIAN = Symbol('LITTLE_ENDIAN');

function getPlatformEndianness() {
	let arr32 = Uint32Array.of(0x12345678);
	let arr8 = new Uint8Array(arr32.buffer);
	switch ((arr8[0]*0x1000000) + (arr8[1]*0x10000) + (arr8[2]*0x100) + (arr8[3])) {
		case 0x12345678:
			return BIG_ENDIAN;
		case 0x78563412:
			return LITTLE_ENDIAN;
		default:
			throw new Error('Unknown endianness');
	}
}
           

與普通數組相比,TypedArray 數組的最大優點就是可以直接操作記憶體,不需要資料類型轉換,是以速度快得多。

BYTES_PER_ELEMENT 屬性

每一種視圖的構造函數,都有一個 BYTES_PER_ELEMENT 屬性,表示這種資料類型占據的位元組數。

Int8Array.BYTES_PER_ELEMENT	//1
Uint8Array.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
           

這個屬性在 TypedArray 執行個體上也能擷取,即有 TypedArray.prototype.BYTES_PER_ELEMENT 。

ArrayBuffer 與字元串的互相轉換

ArrayBuffer 轉為字元串,或者字元串轉為 ArrayBuffer,有一個前提,即字元串的編碼方法是确定的。假定字元串采用UTF-16編碼(JavaScript的内部編碼方式),可以自己編寫轉換函數。

// ArrayBuffer轉為字元串,參數為 ArrayBuffer對象
function ab2str(buf) {
	return String.fromCharCode.apply(null, new Uint16Array(buf));
}

// 字元串專為 ArrayBuffer對象,參數為字元串
function str2ab(str) {
	var buf = new ArrayBuffer(str.length * 2); // 每個字元占用兩個位元組
	var bufView = new Uint16Array(buf);
	for(var i=0,strLen = str.length;i<strLen;i++) {
		bufView[i] = str.charCodeAt(i);
	} 
	return buf;
}
           

溢出

不同的視圖類型,所能容納的數值範圍是确定的。超出這個範圍,就會出現溢出。比如,8位視圖隻能容納一個8位的二進制值,如果放入一個9位數,就會溢出。

TypedArray 數組的溢出處理規則,簡單來說,就是抛棄溢出的位,然後按照視圖類型進行解釋。

var uint8 = new Uint8Array(1);

uint8[0] = 256;
console.log(uint8[0]);   //0

uint8[0] = -1;
console.log(uint8[0]);   //255
           

上面代碼中,uint8 是一個8位視圖,而256的二進制形式是一個9位的值 100 , 000 , 000,這時就會發生溢出。根據規則,隻會保留後8位,即00,000,000。uint8 視圖的解釋規則是無符号的8位整數,是以 00,000,000 就是0。

負數在計算機内部采用“2的補碼”表示,也就是說,将對應的正數值進行否運算,然後加1。比如,-1對應的正值是1,進行否運算以後,得到11,111,110,再加上1就是補碼形式11,111,111。uint8 按照無符号的8位整數解釋11,111,111,傳回結果就是255。

一個簡單轉換規則,可以這樣表示:

    · 正向溢出(overflow):當輸入值大于目前資料類型的最大值,結果等于目前資料類型的最小值加上餘值,再減去1。

    · 負向溢出(underflow):當輸入值小于目前資料類型的最小值,結果等于目前資料類型的最大值減去餘值,再加上1。

var int8 = new Int8Array(1);
int8[0] = 128;
console.log(int8[0]); //-128

int8[0] = -129;
console.log(int8[0]); //127
           

上面例子中,int8 是一個帶符号的8位整數視圖,它的最大值是127,最小值是-128。輸入值為128時,相當于正向溢出1,根據“最小值加上餘值,再減去1”的規則,就會傳回-128;輸入值為-129時,相當于負向溢出1,根據“最大值減去餘值,再加上1”的規則,就會傳回127。

Uint8ClampedArray 視圖的溢出規則,與上面的規則不同。它規定,凡是發生正向溢出,該值一律等于目前資料類型的最大值,即255;如果發生負向溢出,該值一律等于目前資料類型的最小值,即0。

var uint8c = new Uint8ClampedArray(1);
uint8c[0] = 256;
console.log(uint8c[0]);  //255

uint8c[0] = -1;
console.log(uint8c[0]);  //0
           

上面例子中,uint8c是一個Uint8ClampedArray 視圖,正向溢出時都傳回255,負向溢出時都傳回0。

TypedArray.prototype.buffer

TypedArray 執行個體的 buffer 屬性,傳回整段記憶體區域對應的 ArrayBuffer 對象。該屬性為隻讀屬性。

var a = new Float32Array(64);
var b = new Uint8Array(a.buffer);
           

上面代碼的a視圖對象和b視圖對象,對應同一個ArrayBuffer 對象,即同一段記憶體。

TypedArray.prototype.byteLength, TypedArray.prototype.byteOffset

byteLength 屬性傳回 TypedArray 數組占據的記憶體長度,機關為位元組。byteOffset 屬性傳回 TypedArray 數組從底層 ArrayBuffer 對象的哪個位元組開始。這兩個屬性都是隻讀屬性。

var b = new ArrayBuffer(8);
var v1 = new Int32Array(b);
var v2 = new Uint8Array(b, 2);
var v3 = new Int16Array(b, 2, 2);

console.log(v1.byteLength) //8
console.log(v2.byteLength) //6
console.log(v3.byteLength) //4

console.log(v1.byteOffset) //0
console.log(v2.byteOffset) //2 
console.log(v3.byteOffset) //2
           

TypedArray.prototype.length

length 屬性表示 TypedArray 數組含有多少個成員。注意将 byteLength 屬性和 length 屬性區分,前者是位元組長度,後者是成員長度。

var a = new Int16Array(8);
console.log(a.length); //8
console.log(a.byteLength); //16
           

TypedArray.prototype.set( )

TypedArray 數組的 set 方法用于複制數組(普通數組或TypedArray數組),也就是将一段内容完全複制到另一段記憶體。

var a = new Uint8Array(8);
var b = new Uint8Array(8);

b.set(a);
           

上面代碼複制a數組的内容到b數組,它是整段記憶體的複制,比一個個拷貝成員的那種快得多。

set 方法還可以接受第二個參數,表示從b對象的哪一個成員開始複制a對象。

var a = new Uint8Array(8);
var b = new Uint8Array(10);

b.set(a, 2);
           

上面代碼的b數組比a數組多兩個成員,是以從b[2] 開始複制。

TypedArray.prototype.subarray( )

subarray 方法是對于 TypedArray 數組的一部分,再建立一個新的視圖。

var a = new Uint16Array(8);
var b = a.subarray(2, 3);

console.log(a.byteLength); //16
console.log(b.byteLength); //2 
           

subarray 方法的第一個參數是起始的成員序号,第二個參數是結束的成員序号(不含該成員),如果省略則包含剩餘的全部成員。是以,上面代碼的 a.subarray(2, 3) 意味着b隻包含 a[2] 一個成員,位元組長度為2。

TypedArray.prototype.slice( )

TypedArray 執行個體的 slice 方法,可以傳回一個指定位置的新的 TypedArray 執行個體。

let ui8 = Uint8Array.of(0, 1, 2);
ui8.slice(-1);
// Uint8Array [2]
           

上面代碼中,ui8 是8位無符号整數數組視圖的一個執行個體。它的slice方法可以從目前視圖之中,傳回一個新的視圖執行個體。

slice 方法的參數,表示原數組的具體位置,開始生成新數組。負值表示逆向的位置,即-1為倒數第一個位置,-2表示倒數第二個位置,以此類推。

TypedArray.of( )

TypedArray 數組的所有構造函數,都有一個靜态方法of,用于将參數轉為一個 TypedArray 執行個體。

console.log( Float32Array.of(0.151, -8, 2.9) );
// Float32Array [0.151, -8, 2.9]
           

下面三種方法都會生成同樣一個TypedArray數組。

// 方法一
let arr = new Uint8Array([1,2,3]);

// 方法二
let arr = Uint8Array.of(1,2,3);

// 方法三
let arr = new Uint8Array(3);
arr[0] = 1;
arr[1] = 2;
arr[2] = 3; 
           

TypedArray.from( )

靜态方法 from 接受一個可周遊的資料結構(比如數組)作為參數,傳回一個基于這個結構的 TypedArray 執行個體。

console.log( Uint16Array.from([0, 1, 2]) );
// Uint16Array [0, 1, 2]
           

這個方法還可以将一種 TypedArray 執行個體,轉為另一種。

var ui16 = Uint16Array.from( Uint8Array.of(0, 1, 2) );
console.log( ui16 instanceof Uint16Array );
// true
           

from 方法還可以接受一個函數,作為第二個參數,用來對每個元素進行周遊,功能類似map方法。

Int8Array.of(127, 126, 125).map(x => 2 * x);
// Int8Array [-2, -4, -6]

console.log( Int16Array.from(Int8Array.of(127, 126, 125), x => 2 * x ) );
// Int16Array [254, 252, 250]
           

上面的例子中,from 方法沒有發生溢出,這說明周遊不是針對原來的8位整數數組。也就是說,from 會将第一個參數指定的TypedArray 數組,拷貝到另一段記憶體之中,處理之後再将結果轉成指定的數組格式。

複合視圖

由于視圖的構造函數可以指定起始位置和長度,是以在同一段記憶體之中,可以依次存放不同類型的資料,這叫做“複合視圖”。

var buffer = new ArrayBuffer(24);

var idView = new Uint32Array(buffer, 0, 1);
var usernameView = new Uint8Array(buffer, 4, 16);
var amountDueView = new Float32Array(buffer, 20, 1);
           

上面代碼将一個24位元組長度的 ArrayBuffer 對象,分成三個部分:

    · 位元組0到位元組3:1個32位無符号整數

    · 位元組4到位元組19:16個8位整數

    · 位元組20到位元組23:1個32位浮點數

這種資料結構可以用如下的C語言描述:

struct someStruct {
    unsigned long id;
    char username[16];
    float amountDue;
}
           

DtataView 視圖

如果一段資料包括多種類型(比如伺服器傳來的HTTP資料),這時除了建立ArrayBuffer對象的複合視圖以外,還可以通過 DataView 視圖進行操作。

DataView 視圖提供更多操作選項,而且支援設定位元組序。本來,在設計目的上,ArrayBuffer 對象的各種 TypedArray 視圖,是用來向網卡、聲霸卡之類的本機裝置傳送資料,是以使用本機的位元組序就可以了;而 DataView 視圖的設計目的,是用來處理網絡裝置傳來的資料,是以大端位元組序或小端位元組序是可以自行設定的。

DataView 視圖本身也是構造函數,接受一個 ArrayBuffer 對象作為參數,生成視圖。

DataView(ArrayBuffer buffer[, 位元組起始位置[,長度]]);
           

例子:

var buffer = new ArrayBuffer(24);
var 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方法的參數都是一個位元組序号(不能是負數,否則會報錯),表示從哪個位元組開始讀取。

var buffer = new ArrayBuffer(24);
var dv = new DataView(buffer);

//從第1個位元組讀取一個8位無符号整數
var v1 = dv.getUint8(0);

//從第2個位元組讀取一個16位無符号整數
var v2 = dv.getUint16(1);

//從第4個位元組讀取一個16位無符号整數
var v3 = dv.getUint16(3);
           

上面代碼讀取了 ArrayBuffer 對象的前5個位元組,其中有一個8位整數和兩個十六位整數。

如果一次讀取兩個或兩個以上的位元組,就必須明确資料的存儲方式,到底是小端位元組序還是大端位元組序。預設情況下,DataView 的 get 方法使用大端位元組序解讀資料,如果需要使用小端位元組序解讀,必須在 get 方法的第二個參數指定 true。

// 小端位元組序
var v1 = dv.getUint16(1, true);

// 大端位元組序
var v2 = dv.getUint16(3, false);

// 大端位元組序
var 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);
           

如果不确定正在使用的計算機的位元組序,可以采用下面的判斷方式。

var littleEndian = (function() {
    var buffer = new ArrayBuffer(2);
    new DataView(buffer).setInt16(0, 256, true);
    return new Int16Array(buffer)[0] === 256;
})();
           

如果傳回true,就是小端位元組序;如果傳回false,就是大端位元組序。

二進制數組的應用

大量的Web API 用到了ArrayBuffer 對象和它的視圖對象。

AJAX

傳統上,伺服器通過AJAX操作隻能傳回文本資料,即responseType屬性預設為text。XMLHTTPRequest 第二版XHR2允許伺服器傳回二進制資料,這時分兩種情況。如果明确知道傳回的二進制資料類型,可以把傳回類型(responseType)設為 arraybuffer;如果不知道,就設為blob。

var xhr = new XMLHttpRequest();
xhr.open('GET', someUrl);
xhr.responseType = 'arraybuffer';

xhr.onload = function() {
	let arraybuffer = xhr.response;
	// ...
};
xhr.send();
           

如果知道傳回來的是32位整數,可以如下處理:

xhr.onreadystatechange = function() {
	if(req.readyState === 4) {
		var arrayResponse = xhr.response;
		var dataView = new DataView(arrayResponse);
		var ints = new Uint32Array(dataView.byteLength / 4);

		xhrDiv.style.backgroundColor = '#00FF00';
		xhrDiv.innerText = "Array is" + ints.length + "uints long";
	}
}
           

Canvas

網頁Canvas元素輸出的二進制像素資料,就是TypedArray數組。

var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

var imageData = ctx.getImageData(0, 0, cavas.width, canvas.height);
var uint8ClampedArray = imageData.data;
           

需要注意:上面代碼的 uint8ClampedArray 雖然是一個 TypedArray 數組,但是它的視圖類型是一種針對 Canvas 元素的專有類型 Uint8ClampedArray。這個視圖類型的特點,就是專門針對顔色,把每個位元組解讀為無符号的8位整數,即隻能取值0-255,而且發生運算的時候自動過濾高位溢出。這為圖像處理帶來了巨大的友善。

舉例,如果把像素的顔色值設為 Uint8Array 類型,那麼乘以一個 gamma 值的時候,必須這樣計算:

u8[i] = Math.min(255, Math.max(0, u8[i] * gamma));
           

因為 Uint8Array 類型對于大于255的運算結果(比如0xFF+1),會自動變為0x00,是以圖像處理必須要像上面這樣算。這樣做麻煩且影響性能。如果将顔色值設為 Uint8ClampedArray 類型,計算就簡化很多。

pixels[i] *= gamma;
           

Uint8ClampedArray 類型確定将小于0的值設為0,将大于255的值設為255。注意,IE10不支援該類型。

WebSocket

WebSocket 可以通過 ArrayBuffer ,發送或接收二進制資料。

var socket = new WebSocket('ws://127.0.0.1:8081');
socket.binaryType = 'arraybuffer';

// Wait until socket is open
socket.addEventListener('open', function(event) {
	// Send binary data
	var typedArray = new Uint8Array(4);
	socket.send(typedArray.buffer);
});

// Receive binary data
socket.addEventListener('message', function(event) {
	var arraybuffer = event.data;
	// ...
});
           

Fetch API

Fetch API 取回的資料,就是 ArrayBuffer 對象。

fetch(url)
  .then(function(request) {
  	return request.arrayBuffer()
  })
  .then(function(arrayBuffer) {
  	// ...
  });
           

File API

如果知道一個檔案的二進制資料類型,也可以将這個檔案讀取為 ArrayBuffer 對象。

var fileInput = document.getElementById('fileInput');
var file = fileInput.files[0];
var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function() {
	var arrayBuffer = reader.result;
	// ...
}
           

下面以處理bmp檔案為例。假定file變量是一個指向bmp檔案的檔案對象,首先讀取檔案。

var reader = new FileReader();
reader.addEventListener('load', processimage, false);
reader.readAsArrayBuffer(file);
           

然後,定義處理圖像的回調函數:先在二進制資料之上建立一個 DataView 視圖,在建立一個 bitmap 對象,用于存放處理後的資料,最後将圖像展示在 Canvas 元素之中。

function processiamge(e) {
	var buffer = e.target.result;
	var datav = new DataView(buffer);
	var bitmap = {};
	// 具體的處理步驟
}
           

具體處理圖像資料時,先處理bmp的檔案頭。具體每個檔案頭的格式和定義,請參閱有關資料。

bitmap.fileheader = {};
bitmap.fileheader.bfType = datav.getUint16(0, true);
bitmap.fileheader.bfSize = datav.getUint32(2, true);
bitmap.fileheader.bfReserved1 = datav.getUint16(6, true);
bitmap.fileheader.bfReserved2 = datav.getUint16(8, true);
bitmap.fileheader.bfOffBits = datav.getUint32(10, true);
           

接着處理圖像元資訊部分。

bitmap.infoheader = {};
bitmap.infoheader.biSize = datav.getUint32(14, true); 
bitmap.infoheader.biWidth = datav.getUint32(18, true); 
bitmap.infoheader.biHeight = datav.getUint32(22, true); 
bitmap.infoheader.biPlanes = datav.getUint16(26, true); 
bitmap.infoheader.biBitCount = datav.getUint16(28, true); 
bitmap.infoheader.biCompression = datav.getUint32(30, true); 
bitmap.infoheader.biSizeImage = datav.getUint32(34, true); 
bitmap.infoheader.biXPelsPerMeter = datav.getUint32(38, true); 
bitmap.infoheader.biYPelsPerMeter = datav.getUint32(42, true); 
bitmap.infoheader.biClrUsed = datav.getUint32(46, true);
bitmap.infoheader.biClrImportant = datav.getUint32(50, true);
           

最後處理圖像本身的像素資訊。

var start = bitmap.fileheader.bfOffBits;
bitmap.pixels = new Uint8Array(buffer, start);
           

至此,圖像檔案的資料全部處理完成。下一步,根據需要,進行圖像變形,或者轉換格式,或者展示在Canvas網頁元素之中。