Set 與 Map 這類資料結構,在程式設計中經常會使用到,ES6 中正式規範了 Set 與 Map API,雖然還不是完善,然而在某些需求時,確實可以省一些功夫。
首先來看到 Set,它內部的元素不重複:
> let set = new Set([1, 2, 3, 4, 5, 1, 2, 3]);
undefined
> set;
Set { 1, 2, 3, 4, 5 }
> set.add(6);
Set { 1, 2, 3, 4, 5, 6 }
> set.has(3);
true
> set.forEach(elem => console.log(elem));
1
2
3
4
5
6
undefined
> for(let elem of set) {
... console.log(elem);
... }
1
2
3
4
5
6
undefined
> [...set];
[ 1, 2, 3, 4, 5, 6 ]
> let [a, b, c] = set;
undefined
> a;
1
> b;
2
> c;
3
>
Set 本身是可迭代的,因此可以使用 for...of,當然,也可以適用 ... 來 Spread 元素,或者解構語法。
Set 是無序的,因此沒有直接可取值的 get 之類的方法,除了上面方法示範之外,Set 還有 delete 可用來刪除元素,clear 可用來清空元素,size 可用來查看 Set 中的元素數量。
那麼問題來了,Set 中判定元素是否重複的依據是什麼?如果是基本型態,顯然就是值是不是相同,這沒有問題,那麼物件呢?
> let o1 = {x: 10};
undefined
> let o2 = {x: 10};
undefined
> let o3 = o2;
undefined
> let set = new Set([o1, o2, o3]);
undefined
> set;
Set { { x: 10 }, { x: 10 } }
>
在上面的例子中,最後的 set 有兩個物件,顯然地,並不是判定物件的特性實際上是否相等,那麼有 equals、hashCode 之類的協定,可以定義物件實質的內含值是否相同嗎?沒有!對於物件,基本上就是相當於 === 比較。
因為 JavaScript 的物件特性很容易變更,如果你瞭解其他語言中 equals、hashCode 之類的協定,也應該知道,一個狀態可變的物件,在實作 equals、hashCode 之類的協定時會有許多坑,因此就目前來說,Set 是特意這麼做的(並不是忽略了),這會造成一些不便,如果你真的需要能依 equals、hashCode 之類的協定來判定物件相等性,那必須自行實作或者是尋求既有的程式庫。
對於 Set 來說,NaN 是可以判定相等的:
> let set = new Set([NaN, NaN, 0, -0]);
undefined
> set;
Set { NaN, 0 }
>
之後談到 ECMAScript 6 的相等性判定時,會看到像 Object.is 會將 0 與 -0 視為不相等,然而,對於 0、-0,Set 認定是相等的,具體來說,Set 是採用所謂的 SameValueZero 演算來判定相等性,詳情會在下一篇文件中說明。
在談到 ES6 的 Set 時,通常會一併談到 WeakSet,簡單來說,垃圾收集時不會考慮物件是否被 WeakSet 包含著,只要物件沒有其他名稱參考著,就會回收物件,如果 WeakSet 中本來有該物件,會自動消失,這可以用來避免必須使用 Set 管理物件,而後忘了從 Set 中清除而發生記憶體洩漏的問題。
WeakSet 中的元素只能是物件,不能是 number、boolean、string、symbol 等,也不能是 null,由於物件可能被垃圾回收,因此它不能被迭代(也不能使用 forEach)、不能使用 size、clear 方法,只能使用 add、has、delete 方法。
接著來談談 Map,雖然 JavaScript 中的物件,本身就是鍵與值的集合體,然而,鍵的部份基本上就是字串,ES6 中多了個 Symbol 可以做為特性,除此之外,就算使用 [] 指定物件作為鍵,它會取得字串描述作為特性:
> let o = {x: 10};
undefined
> let map = {
... [o] : 'foo'
... };
undefined
> for(let p in map) {
... console.log(p);
... }
[object Object]
undefined
>
在 ES6 中,Map 的鍵可以是物件:
> let o = {x: 10};
undefined
> let map = new Map();
undefined
> map.set(o, 'foo');
Map { { x: 10 } => 'foo' }
> map.set({y : 10}, 'foo2');
Map { { x: 10 } => 'foo', { y: 10 } => 'foo2' }
> for(let [key, value] of map) {
... console.log(key, value);
... }
{ x: 10 } 'foo'
{ y: 10 } 'foo2'
undefined
> map.get(o);
'foo'
> map.delete(o);
true
> map;
Map { { y: 10 } => 'foo2' }
>
Map 卻使用 set 方法?怪怪的!….
Map 本身可迭代,使用 for...of 的話,迭代出來的元素會是個包含鍵與值的物件,也可以使用 ... 來解構。除了以上示範的方法之外,可以使用 has 判定是否具有某個鍵,delete 刪除某鍵(與對應的值),使用 clear 清空 Map,使用 keys 取得全部的鍵,使用 values 取得全部的值,使用 entries 取得鍵值對,使用 size 取得鍵值數量,也可以使用 forEach 等。
建構 Map 時,可以使用陣列,其中必須是 [[鍵, 值], [鍵, 值]] 的形式:
> let map = new Map([['k1', 'v1'], ['k2', 'v2']]);
undefined
> map;
Map { 'k1' => 'v1', 'k2' => 'v2' }
>
Map 中的鍵必須是唯一的,判定的方式是 SameValueZero。
在談到 ES6 的 Map 時,通常會一併談到 WeakMap,簡單來說,垃圾收集時不會考慮物件是否被 WeakMap 作為鍵,只要物件沒有其他名稱參考著,就會回收物件,如果 WeakMap 中本來有該物件作為鍵,會自動消失,這可以用來避免必須使用 Map 管理物件,而後忘了從 Map 中清除而發生記憶體洩漏的問題。
WeakMap 中的鍵只能是物件,不能是 number、boolean、string、symbol 等,也不能是 null,由於鍵物件可能被垃圾回收,因此它不能被迭代(也不能使用 forEach)、不能使用 size、clear 方法,只能使用 get、set、has、delete 方法。

