前面的話
定型數組是一種用于處理數值類型(正如其名,不是所有類型)數據的專用數組,最早是在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); // true console.log(view2.buffer === buffer); // true console.log(view1.byteOffset); // 0 console.log(view2.byteOffset); // 5 console.log(view1.byteLength); // 10 console.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)); // 5 console.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)); // 1535 console.log(view.getInt8(0)); // 5 console.log(view.getInt8(1)); // -1
調用view.getInt16(0)時會讀取視圖中的所有字節并將其解釋為數字1535。如下所示,由每一行的setInt8()方法執行后數組緩沖區的變化,可以理解為何會得到這個結果
new ArrayBuffer(2) 0000000000000000 view.setInt8(0, 5); 0000010100000000 view.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); // true console.log(view2.buffer === buffer); // true console.log(view1.byteOffset); // 0 console.log(view2.byteOffset); // 5 console.log(view1.byteLength); // 10 console.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); // 4 console.log(ints.length); // 2 console.log(floats.byteLength); // 20 console.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); // false console.log(ints1.byteLength); // 4 console.log(ints1.length); // 2 console.log(ints1[0]); // 25 console.log(ints1[1]); // 50 console.log(ints2.byteLength); // 8 console.log(ints2.length); // 2 console.log(ints2[0]); // 25 console.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); // 2 console.log(ints[0]); // 25 console.log(ints[1]); // 50 ints[0] = 1; ints[1] = 2; console.log(ints[0]); // 1 console.log(ints[1]); // 2
在這段代碼中,新創建的Int16Array中有兩個元素,這些元素均通過數值型索引來被讀取和寫入,那些值會自動儲存并轉換成int16類型的值。當然,定型數組與普通數組還有其他相似之處
[注意]可以修改length屬性來改變普通數組的大小,而定型數組的length屬性是一個不可寫屬性,所以不能修改定型數組的大小,如果嘗試修改這個值,在非嚴格模式下會直接忽略該操作,在嚴格模式下會拋出錯誤
【通用方法】
定型數組也包括許多在功能上與普通數組方法等效的方法,以下方法均可用于定型數組
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); // 2 console.log(mapped[0]); // 50 console.log(mapped[1]); // 100 console.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); // true console.log(intsArray[0]); // 25 console.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); // true console.log(floats instanceof Float32Array); // true console.log(ints.length); // 2 console.log(ints[0]); // 25 console.log(ints[1]); // 50 console.log(floats.length); // 2 console.log(floats[0]); // 1.5 console.log(floats[1]); // 2.5
在此示例中,of()方法和from()方法分別創建Int16Array和Float32Array,通過這些方法可以確保定型數組的創建過程如普通數組一樣簡單
不同點
定型數組與普通數組最重要的差別是:定型數組不是普通數組。它不繼承自Array,通過Array.isArray()方法檢查定型數組返回的是false
let ints = new Int16Array([25, 50]); console.log(ints instanceof Array); // false console.log(Array.isArray(ints)); // false
由于變量ints是一個定型數組,因此它既不是Array的實例,也不能被認作是一個數組。做此區分很重要,因為盡管定型數組與普通數組相似,但二者在很多方面的行為并不相同
【行為差異】
當操作普通數組時,其可以變大變小,但定型數組卻始終保持相同的尺寸。給定型數組中不存在的數值索引賦值會被忽略,而在普通數組中就可以
let ints = new Int16Array([25, 50]); console.log(ints.length); // 2 console.log(ints[0]); // 25 console.log(ints[1]); // 50 ints[2] = 5; console.log(ints.length); // 2 console.log(ints[2]); // undefined
在這個示例中,盡管將數值索引2賦值為5,但ints數組尺寸并未增長,賦值被丟棄,length屬性保持不變
定型數組同樣會檢查數據類型的合法性,0被用于代替所有非法值
let ints = new Int16Array(["hi"]); console.log(ints.length); // 1 console.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); // 2 console.log(mapped[0]); // 0 console.log(mapped[1]); // 0 console.log(mapped instanceof Int16Array); // true console.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,100 console.log(subints2.toString()); // 75,100 console.log(subints3.toString()); // 50,75
以上示例中,分別通過原始數組ints創建了3個不同的定型數組。數組subints1是通過克隆ints得到的,故它們包含相同的信息;數組subints2從索引2開始復制數據,所以只包含數組ints的最后兩個元素(75和100);數組subints3由于調用subarray()方法時傳入了起始和結束索引的位置,故subints3只包含數組ints中間的兩個元素
文章列表