ES6的Set與Map

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 集合中的元素,該方法的回調函數接受三個參數:valuekey 以及集合本身。雖然 Set 沒有 key 鍵名,但為了與數組和 MapforEach() 保持一致,依然提供了 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() 方法的回調函數也接受三個參數:valuekey 以及集合本身,這和數組更為接近,只不過數組對應的第二個參數是數值型索引值。

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()