ES6躬行記(13)——型別化陣列
型別化陣列(Typed Array)是一種處理二進位制資料的特殊陣列,它可像C語言那樣直接操縱位元組,不過得先用ArrayBuffer物件建立陣列緩衝區(Array Buffer),再對映到指定格式的檢視(view)之後,才能讀寫其中的資料。總共有兩類檢視,分別是特定型別的TypedArray和通用型別的DataView。在ES6引入型別化陣列之後,大大提升了JavaScript數學運算的效能。
一、ArrayBuffer
雖然ArrayBuffer物件可以開闢一片固定大小的記憶體區域(即陣列緩衝區),但它不能直接讀寫所儲存的資料,需要藉助檢視才行。通過建構函式ArrayBuffer()可以分配指定位元組數量的緩衝區,如下程式碼所示,分配了一段8個位元組的記憶體區域,每個位元組的預設值都為0。有一點要注意,緩衝區的容量在指定後,就不可再修改。
var buffer = new ArrayBuffer(8);
利用ArrayBuffer物件的只讀屬性byteLength,可以獲取緩衝區的位元組長度。而通過它的slice()方法可建立一個指定範圍的緩衝區副本。還有一個靜態方法ArrayBuffer.isView(),用來檢測是否是一個檢視例項,具體如下所示。
buffer.byteLength;//8 buffer.slice(2, 4); ArrayBuffer.isView(buffer);//false
二、TypedArray
1)資料型別
檢視的行為類似於陣列,但並不是真正的陣列。檢視可根據指定的數值型別解讀緩衝區中的二進位制資料,而TypedArray包含9種特定型別的檢視(即型別化陣列),如表8所示(引用自ES6規範的 22.2小節 )。
表8 九種特定型別的檢視
檢視名稱 | 元素大小(位元組) | 描述 | 等價的C語言型別 |
Int8Array | 1 | 8位二進位制補碼有符號整數 | signed char |
Uint8Array | 1 | 8位無符號整數 | unsigned char |
Uint8ClampedArray | 1 | 8位無符號整數(對溢位做特殊處理) | unsigned char |
Int16Array | 2 | 16位二進位制補碼有符號整數 | short |
Uint16Array | 2 | 16位無符號整數 | unsigned short |
Int32Array | 4 | 32位二進位制補碼有符號整數 | int |
Uint32Array | 4 | 32位無符號整數 | unsigned int |
Float32Array | 4 | 32位IEEE浮點數 | float |
Float64Array | 8 | 64位IEEE浮點數 | double |
表中的9種檢視都繼承自TypedArray物件,而每種檢視都只能處理一種數值型別的資料(例如Float32Array只能處理float型別的數值),並且檢視中的元素大小(即所佔的位元組數)也與數值型別有關,例如Float32Array的1個元素包含4個位元組。由於各個檢視的元素都會有規定的數值範圍,因此超出該範圍就會溢位。其中對Uint8ClampedArray的溢位處理較為特殊,它的數值範圍在0到255之間,如果緩衝區所存的值超出該範圍,那麼就會替換這個值,例如小於0的數值被轉換成0,而大於255的數值則被轉換成255,如下所示。
var view = new Uint8ClampedArray(2); view[0] = -1; view[1] = 256; console.log(view);//Uint8ClampedArray(2) [0, 255]
2)建立
每種特定型別的檢視都有一個建構函式,每個建構函式都有4種引數的組合,即共有4種方式建立型別化陣列,具體如下所列,每一種建立方式的後面都會給出相應的示例程式碼。
(1)3個引數,第一個是陣列緩衝區,第二個是可選的位元組偏移量,第三個是可選的需要包含的元素個數。注意,偏移量需要是元素大小的倍數。
var buffer = new ArrayBuffer(16), view1 = new Int16Array(buffer), //Int16Array(8) [0, 0, 0, 0, 0, 0, 0, 0] view2 = new Int16Array(buffer, 4, 2);//Int16Array(2) [0, 0]
利用這組引數,可以讓一個緩衝區關聯多個檢視,如下程式碼所示。
var view1 = new Int8Array(buffer, 0, 4), view2 = new Int16Array(buffer, 4, 4), view3 = new Int32Array(buffer, 12, 1); view1.buffer === view2.buffer;//true view2.buffer === view3.buffer;//true
三個檢視分別佔據了前4個位元組、中間8個位元組以及後4個位元組的緩衝區。檢視的buffer屬性指向了它所處的緩衝區,兩組全等比較的結果都為真,由此可知,它們使用了同一個緩衝區。
(2)1個數值,表示型別化陣列的元素個數,將該引數乘以每個元素的大小即可得到緩衝區的容量,前面描述Uint8ClampedArray特性的示例就用到了這種建立方式。注意,此時建構函式會自動建立合適容量的緩衝區。
var view = new Int16Array(7);//Int16Array(7) [0, 0, 0, 0, 0, 0, 0]
(3)1個型別化陣列,它的元素會被複制到新的型別化陣列中,雖然元素個數保持不變,但使用了不同的緩衝區。
var view1 = new Int16Array(6), view2 = new Int8Array(view1);//Int8Array(6) [0, 0, 0, 0, 0, 0]
(4)1個物件,只要不是TypedArray或ArrayBuffer就行,例如陣列、類陣列等物件。
var view = new Int16Array([1, 2, 3]);//Int16Array(3) [1, 2, 3]
3)與常規陣列的異同
型別化陣列與常規陣列有許多相似點,下面僅列出其中的三點:
(1)都可以通過數字型別的索引來訪問某個位置的元素。
(2)通過length屬性可獲取包含的元素個數。
(3)都包含slice()、of()、from()、copyWithin()等陣列方法。
雖然兩者之間的共性不少,但是型別化陣列的特點又很鮮明,例如:
(1)每種型別化陣列都包含一個BYTES_PER_ELEMENT屬性,能獲取每個元素所佔的位元組(即元素大小),如下程式碼所示。
Int8Array.BYTES_PER_ELEMENT;//1 Int16Array.BYTES_PER_ELEMENT;//2 Int32Array.BYTES_PER_ELEMENT;//4
(2)由於型別化陣列無法維護自己的長度,因此將length屬性定義為只讀,並且缺少pop()、push()、shift()等會更改陣列長度的方法。
(3)包含獨有的屬性和方法,如表9所列。
表9 型別化陣列的屬性和方法
屬性或方法 | 功能描述 |
buffer | 只讀屬性,讀取檢視所在的陣列緩衝區 |
byteOffset | 只讀屬性,讀取位元組偏移量 |
byteLength | 只讀屬性,讀取檢視所佔的位元組長度 |
set() | 從另一個常規陣列或型別化陣列中提取元素,再複製給當前型別化陣列 |
subarray() | 從當前型別化陣列中提取元素,再由這些元素組成一個新的型別化陣列 |
byteOffset屬性等於建構函式的第二個引數,而byteLength屬性等於建構函式的第三個引數與元素大小相乘的積,如下所示。
var buffer = new ArrayBuffer(8), view = new Int16Array(buffer, 2, 3); view.buffer;//ArrayBuffer(8) {} view.byteOffset;//2 view.byteLength;//6
set()方法可接收2個引數,第一個是被提取的常規陣列或型別化陣列,第二個是可選的引數,表示當前型別化陣列的偏移量,即從這個位置開始覆蓋它的元素。subarray()方法也能接收2個引數,但都是可選的索引引數,第一個是開始提取的位置,第二個是結束提取的位置,注意,該位置上的元素不會被提取。關於兩個方法的使用,可參考下面的程式碼。
view.set([8], 1); console.log(view);//Int16Array(3) [0, 8, 0] view.subarray(1, 2);//Int16Array [8]
三、DataView
DataView只有一個身份(即檢視),而之前的型別化陣列有雙重身份,既是檢視,也是類陣列。如果要使用DataView檢視,那麼需要先建立陣列緩衝區,類似於型別化陣列的第一種建立方式,只不過它的建構函式中的第三個可選的引數換成需要包含的位元組長度,如下程式碼所示。根據全等比較的結果可知,兩個檢視處在同一個緩衝區中。
var buffer = new ArrayBuffer(16), view1 = new DataView(buffer), view2 = new DataView(buffer, 4, 6); view1.buffer === view2.buffer;//true
1)寫入和讀取
想要通過DataView檢視讀寫緩衝區的資料,需要先為其指定資料型別,而它支援8種資料型別(除了Uint8Clamped)。DataView提供了8對原型方法,每對方法分別以“set”和“get”作為名稱字首,前者用於寫入,後者用於讀取,在字首的後面會跟著資料型別,例如setInt16()和getInt16()。
寫入方法的第一個引數是位元組偏移量,第二個引數是要定義的數值。而讀取方法的第一個引數也是位元組偏移量,如下程式碼所示,兩張檢視被指定了不同的資料型別。
view1.setInt8(2, 8); view1.getInt8(0);//0 view1.getInt8(2);//8 view2.setInt16(0, 16); view2.getInt16(0);//16 view2.getInt16(2);//0
2)小端序
除了Int8和Uint8型別,其它型別的寫入和讀取方法都還包含一個可選的布林引數:littleEndian,表示是否啟用小端序,預設為true。在瞭解小端序之前,需要先了解一下端序。端序又稱位元組序(Endianness),表示多位元組中的位元組排列方式。小端序是指位元組的最低有效位在最高有效位之前(大端序正好與之相反),例如數字10,如果用16位二進位制表示,那麼它就變為0000 0000 0000 1010,換算成16進位制就是000A,用小端序儲存的話,該值會被表示成0A00。雖然大端序更符合人類的閱讀習慣,但英特爾處理器和多數瀏覽器採用的都是小端序。引入該引數後,能更靈活的處理不同儲存方式的資料。