在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種。
注意:二進制數組并不是真正的數組,而是類似數組的對象。
很多浏覽器操作的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網頁元素之中。