Set 與 Map


Set 與 Map 這類資料結構,在程式設計中經常會使用到,ES6 中正式規範了 SetMap 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 中的元素只能是物件,不能是 numberbooleanstringsymbol 等,也不能是 null,由於物件可能被垃圾回收,因此它不能被迭代(也不能使用 forEach)、不能使用 sizeclear 方法,只能使用 addhasdelete 方法。

接著來談談 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 中的鍵只能是物件,不能是 numberbooleanstringsymbol 等,也不能是 null,由於鍵物件可能被垃圾回收,因此它不能被迭代(也不能使用 forEach)、不能使用 sizeclear 方法,只能使用 getsethasdelete 方法。