標籤:原型 open canvas find 特定 綁定 lam join() 組合
前面的話
定型數組是一種用於處理數實值型別(正如其名,不是所有類型)資料的專用數組,最早是在WebGL中使用的,WebGL是OpenGL ES 2.0的移植版,在Web 頁面中通過 <canvas> 元素來呈現它。定型數組也被一同移植而來,其可為JS提供快速的按位元運算。本文將詳細介紹ES6定型數組
概述
在JS中,數字是以64位浮點格式儲存的,並按需轉換為32位整數,所以算術運算非常慢,無法滿足WebGL的需求。因此在ES6中引入定型數組來解決這個問題,並提供更高效能的算術運算。所謂定型數組,就是將任何數字轉換為一個包含數字位元的數組,隨後就可以通過我們熟悉的JS數組方法來進一步處理
ES6採用定型數組作為語言的正式格式來確保更好的跨JS引擎相容性以及與JS數組的互通性。儘管ES6版本的定型數組與WebGL中的不一樣,但是仍保留了足夠的相似之處,這使得ES6版本可以基於WebGL版本演化而不至於走向完全分化
【數值資料類型】
JS數字按照IEEE 754標準定義的格式儲存,也就是用64個位元來儲存一個浮點形式的數字。這個格式用於表示JS中的整數及浮點數,兩種格式間經常伴隨著數字改變發生相互轉換。定型數組支援儲存和操作以下8種不同的數實值型別
有符號的8位整數(int8)無符號的8位整數(uint8)有符號的16位整數(int16)無符號的16位整數(uint16)有符號的32位整數(int32)無符號的32位整數(uint32)32位浮點數(float32)64位浮點數(float64)
如果用普通的JS數字來儲存8位整數,會浪費整整56個位元,這些位元原本可以儲存其他8位整數或小於56位元的數字。這也正是定型數組的一個實際用例,即更有效地利用位元
所有與定型數組有關的操作和對象都集中在這8個資料類型上,但是在使用它們之前,需要建立一個數組緩衝區儲存這些資料
【數組緩衝區】
數組緩衝區是所有定型數組的根基,它是一段可以包含特定數量位元組的記憶體位址。建立數組緩衝區的過程類似於在C語言中調用malloc()來分配記憶體,只是不需指明記憶體塊所包含的資料類型。可以通過ArrayBuffer建構函式來建立數組緩衝區
let buffer = New ArrayBuffer(10) // 分配10位元組
調用建構函式時傳入數組緩衝區應含的位元數量即可,此樣本中的這條語句建立了一個10位元組長度的數組緩衝區。建立完成後,可以通過byteLength屬性查看緩衝區中的位元數量
let buffer = new ArrayBuffer(10); // 分配了 10 個位元組console.log(buffer.byteLength); // 10
可以通過slice()方法分割已有數組緩衝區來建立一個新的,這個slice()方法與數組上的slice()方法很像:傳入開始索引和結束索引作為參數,然後返回一個新的ArrayBuffer執行個體,新執行個體由原始數組緩衝區的切片組成
let buffer = new ArrayBuffer(10); // 分配了 10 個位元組let buffer2 = buffer.slice(4, 6);console.log(buffer2.byteLength); // 2
在這段代碼中,buffer2建立從索引4和索引5提取的位元組,此處slice()方法的調用與數組版本的類似,傳入的第二個參數不包含在最終結果中
當然,僅建立儲存單元用途不大,除非能夠將資料寫到那個單元中,還需要建立一個視圖來實現寫入的功能
[注意]數組緩衝區包含的實際位元組數量在建立時就已確定,可以修改緩衝區內的資料,但是不能改變緩衝區的尺寸大小
視圖操作
數組緩衝區是記憶體中的一段地址,視圖是用來操作記憶體的介面。視圖可以運算元組緩衝區或緩衝區位元組的子集,並按照其中一種數值型資料類型來讀取和寫入資料。DataView類型是一種通用的數組緩衝區視圖,其支援所有8種數值型資料類型
要使用DataView,首先要建立一個ArrayBuffer執行個體,然後用這個執行個體來建立新的Dataview
let buffer = new ArrayBuffer(10), view = new DataView(buffer);
在此樣本中的view對象可以訪問緩衝區中所有10位元組。如果提供一個表示位元位移量的數值,那麼可以基於緩衝區的其中一部分來建立視圖,DataView將預設選取從位移值開始到緩衝區末尾的所有位元。如果額外提供一個表示選取位元數量的選擇性參數,DataView則從位移位置後選取該數量的位元
let buffer = new ArrayBuffer(10), view = new DataView(buffer, 5, 2); // 包含位置 5 與位置 6 的位元組
這裡的view只能操作位於索引5和索引6的位元組。通過這種方法,可以基於同一個數組緩衝區建立多個view,因而可以為應用申請一整塊獨立的記憶體位址,而不是當需要空間時再動態分配
【擷取視圖資訊】
可以通過以下幾種唯讀屬性來擷取視圖的資訊
buffer 視圖綁定的數組緩衝區byteOffset DataView建構函式的第二個參數,預設是0,只有傳入參數時才有值byteLength DataView建構函式的第三個參數,預設是緩衝區的長度byteLength
通過這些屬性,可以查看視圖正在操作緩衝區的哪一部分
let buffer = new ArrayBuffer(10),view1 = new DataView(buffer), // 包含所有位元組view2 = new DataView(buffer, 5, 2); // 包含位置 5 與位置 6 的位元組console.log(view1.buffer === buffer); // trueconsole.log(view2.buffer === buffer); // trueconsole.log(view1.byteOffset); // 0console.log(view2.byteOffset); // 5console.log(view1.byteLength); // 10console.log(view2.byteLength); // 2
這段代碼一共建立了兩個視圖,view1覆蓋了整個數組緩衝區,view2隻操作其中的一小部分。由於這些視圖都是基於相同的數組緩衝區建立的,因此它們具有相同的buffer屬性,但每個視圖的byteOffset和byteLength屬性又互不相同,這兩個屬性的值取決於視圖運算元組緩衝區的哪一部分
當然,只從記憶體讀取資訊不是很有用,需要同時在記憶體中讀寫資料才能物盡其用
【讀取和寫入資料】
JS有8種數值型資料類型,對於其中的每一種,都能在DataView的原型上找到相應的在數組緩衝區中寫入資料和讀取資料的方法。這些方法名都以set或get打頭,緊跟著的是每一種資料類型的縮寫。例如,以下這個列表是用於讀取和寫入int8和unit8類型資料的方法
getInt8(byteOffset,littleEndian)讀取位於byteOffset後的int8類型資料setInt8(byteOffset, value, littleEndian) 在byteOffset 處寫入int8類型資料getUint8(byteOffset, littleEndian) 讀取位於byteOffset 後的uint8類型資料setUint8(byteOffset, value, littleEndian) 在byteOffset 處寫入uint8類型資料
get方法接受兩個參數:讀取資料時位移的位元組數量和一個可選的布爾值,表示是否按照小端序進行讀取(小端序是指最低有效位元組位於位元組0的位元組順序)。set方法接受三個參數:寫入資料時位移的位元數量、寫入的值和一個可選的布爾值,表示是否按照小端序格式儲存。儘管這裡只展示了用於8位值的方法,但是有一些相同的方法也可用於操作16或32位的值,只需將每一個方法名中的8替換為16或32即可。除所有整數方法外,DataView同樣支援以下讀取和寫入浮點數的方法
getFloat32(byteOffset, littleEndian) 讀取位於byteOffset後的float32類型資料setFloat32(byteOffset,value,littleEndian) 在byteOffset處寫入float32類型資料getFloat64(byteOffset,littleEndian) 讀取位於byteOffset後的float64類型資料setFloat64(byteOffset,value,littleEndian) 在byteOffset處寫入float64類型資料
以下樣本分別展示了set和get方法的實際運用
let buffer = new ArrayBuffer(2), view = new DataView(buffer);view.setInt8(0, 5);view.setInt8(1, -1);console.log(view.getInt8(0)); // 5console.log(view.getInt8(1)); // -1
這段代碼使用兩位元組數組緩衝器來儲存兩個int8類型的值,分別位於位移0和1,每個值都橫跨一整個位元組(8個位元)隨後通過getlnt8()方法將這些值從它們所在的位置提取出來
視圖是獨立的,無論資料之前是通過何種方式儲存的,都可在任意時刻讀取或寫入任意格式的資料。舉個例子,寫入兩個int8類型的值,然後使用int16類型的方法也可以從緩衝區中讀出這些值
let buffer = new ArrayBuffer(2), view = new DataView(buffer);view.setInt8(0, 5);view.setInt8(1, -1);console.log(view.getInt16(0)); // 1535console.log(view.getInt8(0)); // 5console.log(view.getInt8(1)); // -1
調用view.getInt16(0)時會閱讀檢視中的所有位元組並將其解釋為數字1535。如下所示,由每一行的setInt8()方法執行後數組緩衝區的變化,可以理解為何會得到這個結果
new ArrayBuffer(2) 0000000000000000view.setInt8(0, 5); 0000010100000000view.setInt8(1, -1); 0000010111111111
起初,數組緩衝區所有16個位元的值都是0,通過setInt8()方法將數字5寫入第一個位元組,其中兩個數字0會變為數字1(8位元表示下的5是00000101)將-1寫入第二個位元組,所有位元都會變為1,這也是-1的二進位補碼錶示。第一次調用setInt8()後,數組緩衝區共包含16個位元,getInt16()會將這些位元讀作一個16位整型數字,也就是十進位的1535
當混合使用不同資料類型時,DataView對象是一個完美的選擇,然而,如果只使用某個特定的資料類型,那麼特定類型的視圖則是更好的選擇
【定型數組是視圖】
ES6定型數組實際上是用於數組緩衝區的特定類型的視圖,可以強制使用特定的資料類型,而不是使用通用的DataView對象來運算元組緩衝區。8個特定類型的視圖對應於8種數值型資料類型,uint8的值還有其他選擇
“構造器名稱”一列列舉了幾個定型數組的建構函式,其他列描述了每一個定型數組可包含的資料。Uint8ClampedArray與uint8Array大致相同,唯一的區別在於數組緩衝區中的值如果小於0或大於255,uint8ClampedArray會分別將其轉換為0或255,例如,-1會變為0,300會變為255
定型數組操作只能在特定的資料類型上進行,例如,所有Int8Array的操作都使用int8類型的值。定型數組中元素的尺寸也取決於數組的類型,Int8Array中的元素佔一個位元組,而Float64Array中的每個元素佔8位元組。所幸的是,可以像正常數組一樣通過數值型索引來訪問元素,從而避免了調用DataView的set和get方法時的尷尬場面
【建立特定類型的視圖】
定型數組建構函式可以接受多種類型的參數,所以可以通過多種方法來建立定型數組。首先,可以傳入DataView建構函式可接受的參數來建立新的定型數組,分別是數組緩衝區、可選的位元位移量、可選的長度值
let buffer = new ArrayBuffer(10), view1 = new Int8Array(buffer), view2 = new Int8Array(buffer, 5, 2);console.log(view1.buffer === buffer); // trueconsole.log(view2.buffer === buffer); // trueconsole.log(view1.byteOffset); // 0console.log(view2.byteOffset); // 5console.log(view1.byteLength); // 10console.log(view2.byteLength); // 2
在這段代碼中,兩個視圖均是通過buffer產生的Int8Array執行個體,view1和view2有相同的buffer、byteOffset和byteLength屬性,DataView的執行個體包含這三種屬性。當你使用DataView時,只要希望只處理一種數實值型別,總是很容易切換到相應的定型數組
建立定型數組的第二種方法是調用建構函式時傳入一個數字。這個數字表示分配給數組的元素數量(不是位元數量),建構函式將建立一個新的緩衝區,並按照數組元素的數量來分配合理的位元數量,通過length屬性可以訪問數組中的元素數量
let ints = new Int16Array(2), floats = new Float32Array(5);console.log(ints.byteLength); // 4console.log(ints.length); // 2console.log(floats.byteLength); // 20console.log(floats.length); // 5
ints數組建立時含有兩個空元素,每個16位元整型值需要兩個位元組,因而分配了4位元組給該數組;floats數組建立時含有5個空元素,每個元素佔4位元組,所以共需要20位元組。在這兩種情況下,如果要訪問新建立的緩衝區,則可以通過buffer屬性來實現
[注意]調用定型數組的建構函式時如果不傳參數,會按照傳入0來處理,這樣由於緩衝區沒有分配到任何位元,因而建立的定型數組不能用來儲存資料
第三種建立定型數組的方法是調用建構函式時,將以下任一對象作為唯一的參數傳入
1、一個定型數組
該數組中的每個元素會作為新的元素被複製到新的定型數組中。例如,如果將一個int8數組傳入到Int16Array建構函式中,int8的值會被複製到一個新的int16數組中,新的定型數組使用新的數組緩衝區
2、一個可迭代對象
對象的迭代器會被調用,通過檢索所有條目來選取插入到定型數組的元素,如果所有元素都是不適用於該檢視類型的無效類型,建構函式將會拋出一個錯誤
3、一個數組
數組中的元素會被複製到一個新的定型數組中,如果所有元素都是不適用於該檢視類型的無效類型,建構函式將會拋出一個錯誤
4、一個類數組對象
與傳入數組的行為一致
在每個樣本中,新建立的定型數組的資料均取自來源物件,這在用一些值初始化定型數組時尤為有用
let ints1 = new Int16Array([25, 50]), ints2 = new Int32Array(ints1);console.log(ints1.buffer === ints2.buffer); // falseconsole.log(ints1.byteLength); // 4console.log(ints1.length); // 2console.log(ints1[0]); // 25console.log(ints1[1]); // 50console.log(ints2.byteLength); // 8console.log(ints2.length); // 2console.log(ints2[0]); // 25console.log(ints2[1]); // 50
在此樣本中建立了一個Int16Array並用含兩個值的數組進行初始化,然後用Int16Array作為參數建立一個Int32Arpay,由於兩個定型數組的緩衝區完全獨立,因此值25和50從ints1被複製到了ints2。在兩個定型數組中有相同的數字,只是ints2用8位元組來表示資料,而ints1隻用4位元組
相同點
定型數組和普通數組有幾個相似之處,在許多情況下可以按照普通數組的使用方式去使用定型數組。例如,通過length屬性可以查看定型數組中含有的元素數量,通過數值型索引可以直接存取定型數組中的元素
let ints = new Int16Array([25, 50]);console.log(ints.length); // 2console.log(ints[0]); // 25console.log(ints[1]); // 50ints[0] = 1;ints[1] = 2;console.log(ints[0]); // 1console.log(ints[1]); // 2
在這段代碼中,新建立的Int16Array中有兩個元素,這些元素均通過數值型索引來被讀取和寫入,那些值會自動儲存並轉換成int16類型的值。當然,定型數組與普通數組還有其他相似之處
[注意]可以修改length屬性來改變普通數組的大小,而定型數組的length屬性是一個不可寫屬性,所以不能修改定型數組的大小,如果嘗試修改這個值,在非strict 模式下會直接忽略該操作,在strict 模式下會拋出錯誤
【通用方法】
定型數組也包括許多在功能上與普通數組方法等效的方法,以下方法均可用於定型數組
copyWithin()entries()fill()filter()find()findIndex()forEach()indexOf()join()keys()lastIndexOf()map()reduce()reduceRight()reverse()slice()some()sort()values()
儘管這些方法與Array.prototype中的很像,但並非完全一致,定型數組中的方法會額外檢查數實值型別是否安全,也會通過Symbol.species確認方法的傳回值是定型數組而非普通數組
let ints = new Int16Array([25, 50]), mapped = ints.map(v => v * 2);console.log(mapped.length); // 2console.log(mapped[0]); // 50console.log(mapped[1]); // 100console.log(mapped instanceof Int16Array); // true
這段代碼使用map()方法建立一個存放整數的新數組,並通過map()方法將數組中的每個值乘以2,最後返回一個新的Int16Array類型的數組
【相同的迭代器】
定型數組與普通數組有3個相同的迭代器,分別是entries()方法、keys()方法和values()方法,這意味著可以把定型數組當作普通數組一樣來使用展開運算子、for-of迴圈
let ints = new Int16Array([25, 50]), intsArray = [...ints];console.log(intsArray instanceof Array); // trueconsole.log(intsArray[0]); // 25console.log(intsArray[1]); // 50
這段代碼建立了一個名為intsArray的新數組,包含與定型數組ints相同的資料。展開運算子能夠將可迭代對象轉換為普通數組,也能將定型數群組轉換為普通數組
【of()方法和from()方法】
所有定型數組都含有靜態of()方法和from()方法,運行效果分別與Array.of()方法和Array.from()方法相似,區別是定型數組的方法返回定型數組,而普通數組的方法返回普通數組
let ints = Int16Array.of(25, 50), floats = Float32Array.from([1.5, 2.5]);console.log(ints instanceof Int16Array); // trueconsole.log(floats instanceof Float32Array); // trueconsole.log(ints.length); // 2console.log(ints[0]); // 25console.log(ints[1]); // 50console.log(floats.length); // 2console.log(floats[0]); // 1.5console.log(floats[1]); // 2.5
在此樣本中,of()方法和from()方法分別建立Int16Array和Float32Array,通過這些方法可以確保定型數組的建立過程如普通數組一樣簡單
不同點
定型數組與普通數組最重要的差別是:定型數組不是普通數組。它不繼承自Array,通過Array.isArray()方法檢查定型數組返回的是false
let ints = new Int16Array([25, 50]);console.log(ints instanceof Array); // falseconsole.log(Array.isArray(ints)); // false
由於變數ints是一個定型數組,因此它既不是Array的執行個體,也不能被認作是一個數組。做此區分很重要,因為儘管定型數組與普通數組相似,但二者在很多方面的行為並不相同
【行為差異】
當操作普通數組時,其可以變大變小,但定型數組卻始終保持相同的尺寸。給定型數組中不存在的數值索引賦值會被忽略,而在普通數組中就可以
let ints = new Int16Array([25, 50]);console.log(ints.length); // 2console.log(ints[0]); // 25console.log(ints[1]); // 50ints[2] = 5;console.log(ints.length); // 2console.log(ints[2]); // undefined
在這個樣本中,儘管將數值索引2賦值為5,但ints數組尺寸並未增長,賦值被丟棄,length屬性保持不變
定型數組同樣會檢查資料類型的合法性,0被用於代替所有非法值
let ints = new Int16Array(["hi"]);console.log(ints.length); // 1console.log(ints[0]); // 0
這段代碼嘗試向Int16Array數組中添加字串值"hi",字串在定型數組中屬於非法資料類型,所以該值被轉換為0插入數組,數組的長度仍然為1,ints[0]包含的值為0
所有修改定型數組值的方法執行時都會受到相同限制,例如,如果給map()方法傳入的函數返回非法值,則最終會用0來代替
let ints = new Int16Array([25, 50]), mapped = ints.map(v => "hi");console.log(mapped.length); // 2console.log(mapped[0]); // 0console.log(mapped[1]); // 0console.log(mapped instanceof Int16Array); // trueconsole.log(mapped instanceof Array); // false
這裡的字串"hi"不是16位整數,所以在結果數組中會用0來替代它。由於有了這種錯誤更正的特性,故非法資料將不會在數組中出現,即使混入非法資料也不會拋出錯誤
【缺失的方法】
儘管定型數組包含許多與普通數組相同的方法,但也缺失了幾個。以下方法在定型數組中不可使用
concat()pop()push()shift()splice()unshift()
除concat()方法外,這個列表中的方法都可以改變數組的尺寸,由於定型數組的尺寸不可更改,因而這些方法不適用於定型數組。定型數組不支援concat()方法是因為兩個定型數組合并後的結果(尤其當兩個數組分別處理不同資料類型時)會變得不確定,這直接違背了使用定型數組的初衷
【附加方法】
定型數組中還有兩個沒出現在普通數組中的方法set()和subarray()。這兩個方法的功能相反,set()方法將其他數組複製到已有的定型數組,subarray()提取已有定型數組的一部分作為一個新的定型數組
set()方法接受兩個參數:一個是數組(定型數組或普通數組都支援);一個是可選的位移量,表示開始插入資料的位置,如果什麼都不傳,預設的位移量為0。合法資料從作為參數傳入的數組複製至目標定型數組中
let ints = new Int16Array(4);ints.set([25, 50]);ints.set([75, 100], 2);console.log(ints.toString()); // 25,50,75,100
這段代碼建立了一個含有4個元素的數組Int16Array,先調用set()方法將兩個值分別複製到前兩個位置,再次調用set()方法並傳入位移量2,將另外兩個值複製到數組的後兩個位置
subarray()方法接受兩個參數:一個是可選的開始位置,一個是可選的結束位置(與slice()方法的結束位置一樣,不包含當前位置的資料),最後返回一個新的定型數組。也可以省略這兩個參數來複製一個新的定型數組
let ints = new Int16Array([25, 50, 75, 100]), subints1 = ints.subarray(), subints2 = ints.subarray(2), subints3 = ints.subarray(1, 3);console.log(subints1.toString()); // 25,50,75,100console.log(subints2.toString()); // 75,100console.log(subints3.toString()); // 50,75
以上樣本中,分別通過原始數組ints建立了3個不同的定型數組。數組subints1是通過複製ints得到的,故它們包含相同的資訊;數組subints2從索引2開始複製資料,所以只包含數組ints的最後兩個元素(75和100);數組subints3由於調用subarray()方法時傳入了起始和結束索引的位置,故subints3隻包含數組ints中間的兩個元素
ES6定型數組