在JavaScript當中,直到ES5為止,都只有陣列,到了ES6之後持續對陣列進行加強,並加入了ArrayBuffer、TypedArray等,在字典與集合方面,提供Map、WeakMap、Set、WeakSet等類型。因此,開發者應認識這些API特性,在適當場合善加利用,不過,需留意API之間的不一致。

熟悉又陌生的陣列

JavaScript的陣列,本質上就是物件,索引其實就是使用數字作為特性名稱,物件若以數字為特性名稱並加上length,就成了類陣列(Array-like)物件。

若將物件的__proto__指定為Array.prototype,類陣列物件甚至會被instanceof視為Array實例,功能上,就差在無法依元素個數自行維護length值;在其他語言,陣列通常不被視為群集,然而在JavaScript中,由於Array.prototype定義了shift、unshift、push、pop等函式,使得陣列本身可以不只是陣列,也能作為列表(list)、堆疊(stack)來使用。

另一方面,JavaScript本身具備函數式的風格,而在Array.prototype物件上,也定義了filter、map、reduce、some、every等具備函數式風格的函式,未來ES10也預計新增具備Monad概念的flatMap函式。若能善用這類函數式風格函式,不單是程式碼的可讀性,在效率上也可以有所改進。

JavaScript開發者對於陣列,或許很熟悉,然而由於歷史性的原因,陣列使用上有不少的坑。最經典的就是sort函式,預設排序的順序,竟然是根據元素轉字串後的Unicode碼點(code point),而且排序結果是否穩定(Stable),一直都是依實作環境而定,由於大部份主流瀏覽器在實作上都採穩定排序,目前TC39中出現了提案,考慮將穩定排序納入規範之中(https://bit.ly/2QHnVEf)。

陣列API在處理NaN、undefined或空項目(empty item)時,也必須留意不一致的行為。indexOf、lastIndexOf無法處理NaN,因為它們使用===來比對元素,而NaN不等於NaN,陣列中若有NaN作為元素,這兩個函式無法找到它;ES6新增了findIndex,可以使用arr.findIndex(elem => elem !== elem)來尋找NaN,因為NaN是唯一不等於自身的值,另外,ES6新增的includes也可以處理NaN,若陣列arr中有NaN,arr.includes(NaN)會是true。

對於陣列具有undefined與空項目的情況,不少開發者會混淆,例如,搞不清楚 [1, undefined, 3]與[1,,3]的差別。

試著用1 in來測試看看兩者吧!前者確實有索引1的元素,後者沒有;實務開發上,應該避免陣列中出現空項目,因為API對空項目的處理方式並不一致,例如,filter、every等會跳過空項目,不會視為undefined傳入回呼函式,map雖然也不會傳入undefined,然而最後結果會保留空項目(而非undefined),join卻會將空項目視為undefined等。

處理位元組資料

使用XMLHttpRequest Level 1、Fetch API時,若要接受位元組資料,就會接觸到ArrayBuffer、TypedArray等介面。這些介面包含在ES6中,然而它們其實更早是WebGL規範的一部份,為了JavaScript與GPU之間位元組資料處理的需求而存在。

真正用來儲存位元組資料的是ArrayBuffer,就真的只代表位元組資料,除了byteLength可以得知長度,slice方法能夠進行切割之外,沒什麼能做的。怎麼看待這組位元組資料,視開發者選擇哪個TypedArray而定,但TypedArray不是類型,只是一組類型的統稱,像是Int8Array、Uint8Array、Int16Array、Float32Array等,而這些是類陣列,然而也擁有與Array相似的函式,像是filter、map、forEach等。

ArrayBuffer單純就代表著一組位元組,TypedArray才擁有操作位元組的觀點(View),一個ArrayBuffer不單只能有一個TypedArray觀點,TypedArray可以選定ArrayBuffer任何一個區段來操作,因此,一個ArrayBuffer實例,可以含有多種結構的資料,使用多個TypedArray來操作。

如果需要更隨意地處理ArrayBuffer中的位元組資料,可以用DataView來看待ArrayBuffer,它提供getInt8、setFloat32等更靈活的方法,可以從指定索引處,以指定的觀點來設定與取得資料,如果需要確認ArrayBuffer採用的位元組順序(Endianness),是大端序(big-endian)或小端序(little-endian),我們也可以使用DataView,例如:

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

主流瀏覽器與Node.js採用小端序,然而,網站回應的資料或許不是,如果上頭的程式片段略做修改,令buffer代表著接受到的位元組資料,就可以用來判斷回應資料的位元組順序。

字典與集合

JavaScript很長一段時間,沒有內建字典這類資料結構,原因或許在於其物件本身,本質上就是個鍵值複合體,搭配in、delete、[]等,也能達到字典的相關操作,只不過鍵只能是字串,因為JavaScript在相等性方面,並沒有像其他語言中,有equals、hashCode之類的協定,就算鍵只能是字串,也已經滿足許多場合時的字典操作需求。

然而,物件終究不是字典類型,在ES6新增了Map,可以使用物件作為鍵,Map本身也可以進行迭代,在需要字典對應的場合時,ES6之後應該選擇Map而不是物件;另外,ES6也新增了Set,作為元素不得重複的集合操作時,使用上還算方便。

不得重複,正是使用Map、Set時必須注意的。Map的鍵不得重複,Set的元素不得重複,那麼,開發者知道判定重複的標準是什麼嗎?因為歷史性的原因,JavaScript有四種相等性的判定方式,分別是抽象相等、嚴格相等、SameValue與SameValueZero演算,不同的API採用的相等性判斷,並不相同,這在先前專欄〈ECMAScript 6相等演算〉曾經討論,而Map、Set採用的是SameValueZero演算。

談到Map、Set時,通常也會談到ES6新增的WeakMap、WeakSet,簡單來說,垃圾收集時,不會考慮物件是否被WeakMap作為鍵,或者是否為WeakSet的元素,只要物件沒有其他名稱參考著,就會回收物件,在一些場合可避免記憶體洩漏狀況。

例如,若要建立與物件對應的資料,物件可作為Map的鍵,對應資料為值的部份,但須在物件不使用時,自行刪除Map中的鍵值,開發者一疏忽這點,就會發生記憶體洩漏,若用WeakMap可避免。

在撰寫本文的這個時間點,TC39有個處於階段2的WeakRef提案(https://bit.ly/2MqtJDO),當一個變數或特性參考至WeakRef實例時,並不會被計入參考計數,垃圾收集時,可以回收該物件,而其應用的場合之一,是可以將WeakRef實例作為Map的值,實現快取的功能。

留意API的不一致

判斷一門語言是否越來越成熟,從內建的群集API往往可見一斑。從這點來看,JavaScript確實朝著越來越成熟的道路前進,開發者應多加留意與善用。

然而,JavaScript畢竟是從不成熟狀態逐步發展而來,由於歷史性的原因,有些特性令API在使用上,有著奇怪甚至不一致的結果。所以,開發者在使用前,應瞭解並測試API的行為確實符合所需,畢竟,與不一致為伍,也是使用JavaScript必要的課題!

專欄作者

熱門新聞

Advertisement