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
方法。