ES6的Set與Map
- 2020 年 3 月 9 日
- 筆記
Set 和 Map 出現之前
在 ES5 中經常用對象來模擬實現 Set 集合與 Map 集合這兩種數據結構,但這種做法帶來了一些問題:比如利用 if(obj.size)
檢查集合中是否存在某個元素的時候,預期行為是只要存在 size
就能通過 if 判斷,但如果 size = 0
,那麼也無法繼續執行,即使此時元素是存在的。另外,對象的鍵名只能是字符串,非字符串類型的鍵名也會通過 toString()
方法被轉換成字符串,這意味着 obj[5]
與 obj['5']
沒有區別,儘管我們本意是想創建兩個不同的鍵;甚至,當鍵名是對象的時候,不管我們操作的是哪個鍵名(obj[key1]
或者 obj[key2]
),實際操作的都是 obj['[object Object]']
,這是因為對象會被轉換成字符串 '[object Object]'
,這些都是與我們的預期不符合的。因此,ES6 推出了正式的 Set 和 Map 集合。
Set
調用 new Set()
可以創建一個 Set 集合,之後通過 add()
添加元素,size
訪問元素數量。
let set = new Set() set.add('one') set.add('two') set.size // 2
與 ES5 中對象模擬實現不同的是,Set 集合會對添加進來的元素調用 Object.is()
檢查是否一致,由於 5
與 '5'
是不同的,所以 Set 可以同時存在這兩個元素,不會發生類型轉換(唯一的例外是 +0
和 -0
,儘管 Object.is(+0,-0)
返回 false
,但是 Set 集合認為這兩個是一致的);同理,也可以添加多個獨立對象,它們是不會被轉換成字符串的。
set.add(5) set.add('5') let key1 = {} let key2 = {} set.add(key1) set.add(key2) set.size // 4
還可以用 has()
檢測是否存在某個元素,用 delete()
移除指定元素,用 clear()
清空整個 Set 集合。
另外,創建 Set 集合的時候支持傳參,我們可以使用像數組這樣的可迭代對象來初始化 Set 集合(這也是將數組轉換成 Set 集合的方法):
let set = new Set([1,2,2,3,4]) set.size // 4
注意:Set 集合為了保證集合元素唯一,會對數組進行元素過濾,這一點稍後可以用來進行數組去重。
那麼如何訪問集合元素呢?由於 Set 集合沒有鍵名,所以不可能像數組那樣通過數值型索引值去訪問某個元素,要訪問 Set 集合的元素,我們需要先將集合轉換成數組。這個使用展開運算符 ...
來實現:
let set = new Set() set.add(1) set.add(2) let array = [...set] array // [1,2]
數組轉換成集合+集合轉換成數組,就可以實現數組去重:
let array = [1,2,2,3,4,5,6,6] let anotherArray = [...new Set(array)] // [1,2,3,4,5,6]
此外,可以用 forEach()
來迭代 Set 集合中的元素,該方法的回調函數接受三個參數:value
,key
以及集合本身。雖然 Set 沒有 key
鍵名,但為了與數組和 Map
的 forEach()
保持一致,依然提供了 key
參數,它的值與 value
是一樣的。
Weak Set
上面的 Set 是一個強引用的集合,這指的是,如果集合中存儲着對象的引用(set.add(obj)
),那麼即使我們已經在集合外面清除了對該對象的引用(obj = null
),集合中的引用也不受影響。為了避免造成內存泄漏,我們需要一種弱引用的集合,也就是 Weak Set。
Weak Set 只存儲對象的弱引用,所以如果把集合外面對象的最後一個強引用移除,則 Weak Set 中的引用也不復存在,這樣就避免了內存泄露的情況。此外,它還有一些特點:
- 不可以存儲原始值,否則報錯
- 不可迭代,所以不能使用
forEach()
,clear()
- 不支持
size
屬性 - 不暴露諸如
keys()
,values()
等迭代器
Map
相比 Set,Map 存儲的是多對鍵值對,並且鍵名和鍵值支持所有的數據類型。
調用 new Map()
可以創建一個 Map 集合,之後通過 map.set(key,value)
添加鍵值對,map.get(key)
訪問指定鍵名的鍵值。
let map = new Map() let obj = {} map.set('name','Jack') map.set(obj,'I am object') // 不同於對象,在 Map 中鍵名可以是對象 map.get('name') // 'Jack' map.get(obj) // 'I am object' map.get('unexisted key') // 訪問不存在的鍵,返回 undefined
Map 同樣也有 has(key)
,delete(key)
,clear()
,size
(返回鍵值對對數)等方法和屬性。
另外,可以使用數組來初始化 Map 集合,批量添加元素。由於 Map 中的元素是鍵值對,所以傳入的數組的元素也是數組:
let map = new Map([['name','Jack'],['age',12]]) map.get('name') // 'Jack' map.get('age') // 12 map.size // 2
Map 的 forEach()
方法的回調函數也接受三個參數:value
,key
以及集合本身,這和數組更為接近,只不過數組對應的第二個參數是數值型索引值。
Weak Map
類似的,Map 也有弱引用集合 Weak Map。Weak Map 的鍵名必須是對象,且保存着對象的弱引用(如果集合外面引用被清除,則集合中的引用也不復存在,且鍵值對會跟着被移除);鍵值則不一定是對象,且當鍵值是對象時,它保存的依然是強引用。也就是說,Weak Map 的弱引用是針對鍵名來說的。
Weak Map 可以用來跟蹤對象的引用,進而確保將來某一刻需要清除的對象的內存一定能夠得到釋放,不發生潛在的內存泄露。
舉例來說,現在有一個 DOM 元素,它接受用戶的輸入並將輸入的信息存儲在一個對象中,如果沒有使用 Weak Map ,那麼維繫 DOM 對象 與 輸入信息對象 的映射關係時就有可能產生一個新的關於 DOM 對象的強引用,而在之後清除 DOM 對象原先的強引用時,該強引用可能不會被清除,這導致對象內存實際沒有得到釋放。但是,如果使用了 Weak Map,將 DOM 對象作為鍵名,輸入信息對象作為鍵值,那麼由於 Weak Map 存儲的是對象的弱引用,此時就一定能保證 DOM 對象被移除後(且集合外圍對象的最後一個強引用被清除),其內存能夠得到釋放,不會發生內存泄露的問題。
此外,Weak Map 也可以用來存儲對象實例的私有變量:
let Person = (function(name,age){ let privateData = new WeakMap() function Person(){ privateData.set(this,{name:name,age:age}) } Person.prototype.getName = function(){ return privateData.get(this).name } Person.prototype.getAge = function(){ return privateData.get(this).age } }())
在上面的這段代碼中,存在着一個 Weak Map(privateData)用來維繫多個實例與自身私有變量的映射關係。每次創建新實例的時候,都會往 privateData 這個集合中添加新的」映射條目「(privateData.set(this,{name:name,age:age})
,其中,this
指的是實例),鍵名是實例,鍵值是存儲私有變量的對象。這麼一來,當未來某一天刪除實例的時候,由於集合外圍的實例對象的強引用被移除,Weak Map 存儲的又是實例對象的弱引用,所以保證了實弱引用也會被垃圾回收,不存在內存泄漏的問題。簡而言之,只要實例被銷毀,相關信息也會跟着銷毀,這樣就保證了信息的私有性。
此外,Weak Map 還有一些特點:
- 不支持
size
屬性 - 不可迭代,因此不支持
forEach()
和clear()