Set & Map:新生的數據集合及其弱引用衍生

前言

ES6新增了兩種基本的原生數據集合:SetMap(加上ArrayObject如今共有四種),以及由二者衍生出的弱引用集合:WeakSetWeakMap。從某個不無狹隘的角度看(不無狹隘?到底有多狹隘多不狹隘呢?),Set更爲相似Array集合的某種提高,而Map則爲Object集合的加強,雖然兩類在本質上就不相同。html

1 Set

其自己是生成Set實例(數據集合)的構造函數,能夠接受一個數組(或具備iterable接口的數據結構)做爲參數用來初始化。存儲的結構相似數組,不過成員的值在集合中都是惟一的,不會出現重複值。實際的存儲順序(也是遍歷順序)與插入順序一致,行爲的結果和數組相同。segmentfault

其數據結構中沒有鍵名,但爲了和Map統一,也可認爲鍵名和健值是同一值(會在遍歷小節中介紹)。內部使用的相等判斷規則,除了認爲NaN等於NaN,與全等於一致。利用Set中沒有重複值的特性,能夠簡單的實現數組去重。最後一點,其實例的字符串標籤爲[Object Set]——厲害啦,都自立了門戶。數組

let set = new Set([1, 2]);
console.log(...set); // 1 2
console.log({}.toString.call(set)); // [Object Set]

let o = {};
[...new Set([1, 2, 2])]; // [1, 2]
[...new Set([1, NaN, NaN])]; // [1, NaN]
[...new Set([1, o, o])]; // [1, o]
[...new Set([1, {}, {}])]; // [1, {}, {}]

F(1, {}, 1); // [1, {}],可解析帶遍歷器的類數組對象。
function F() {
  console.log([...new Set(arguments)]);
}

let o = { length: 0 };
[...new Set(o)]; // 報錯,不能自動解析不帶遍歷器接口的類數組對象。

// 做爲簡單的刪除數組重複值的方法。
removeDuplicateValues([1, 2, 2, 3]); // [1, 2, 3]
function removeDuplicateValues(arr) {
  return [...new Set(arr)];
}

關於相等的小知識
兩值是否相等通常指的是是否全等,裏面有兩個比較特殊的例子:NaN & NaN-0 & +0。在全等中,NaN不等於NaN-0等於+0都爲零。但這兩種認定在某些場合中不太接地氣,爲此ES6給出用於判斷兩值是否相等方法Object.is()中,認定NaN等於NaN-0不等於+0數據結構

0除以1爲正零,0除以負1爲負零,二者在生成上方式上看的確不該該相等。app

非數NaN是一個不是數字的數字(類型依舊爲數字型),沒有具體的值,所以兩個NaN是不相等的。不過在用NaN做爲映射中的鍵時,它應該代指這一類型而不是具體的個體。否者我先設置代碼NaN指向貂蟬,再設置NaN指代西施,晚上寬衣解帶後發現僕人竟將兩人同時安置在被窩之中,笑盈盈水靈靈的。這,讓我如何是好!dom

2 Map

其自己是生成Map實例(數據集合)的構造函數,能夠接受一個包含鍵值對的數組(或具備iterable接口的數據結構)做爲參數用來初始化。簡單的說,鍵值對是包含兩元素的數組,前者爲鍵名後者爲鍵值。其存儲的結構相似Object,不會出現重複的鍵名,之中使用的相等斷定方法與Set一致。其實例的字符串標籤爲[Object Map]函數

其與對象主要有兩點不一樣。一是鍵名,對象的鍵名只能是字符串或Symbol值,而Map能夠是任意類型,它提供了更爲完善的值對值的Hash結構。二是遍歷順序,對象的遍歷順序大體爲先數值再字符串後Symbol值(會在遍歷小節中介紹),而Map是簡單的與存儲順序保持一致,這在實際操做中比較有用。優化

let map = new Map([[1, 'one'], [2, 'two']]);
console.log(...map); // [1,'one'] [2, 'two']
console.log({}.toString.call(map)); // [Object Map]

let o = {};
[...new Map([[o, 1], [o, 2]])]; // [[o, 2]]
[...new Map([[{}, 1], [{}, 2]])]; // [[{}, 2], [{}, 2]]
[...new Map([[null, 1], [undefined, 2]])]; // [[null, 1], [undefined, 2]]

let o = {
  0: [1, '1'],
  1: [2, '2'],
  length: 2,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
[...new Map(o)]; // [[1, '1'], [2, '2']]

3 實例方法

關於SetMap的完善API請點擊連接查看。this

這裏會將SetMap放置在一塊兒,在操做方法和遍歷方法上進行異同性說明,方便區分和記憶。另外,爲了在方法操做上統一SetMap,如Set小節中說起的,咱們能夠認爲Set是鍵名與鍵值爲同一值的存在(JS自己就是這樣作的)。prototype

3.1 操做方法

二者都有的操做方法
判斷(has),傳入鍵名,返回布爾值。
刪除(delete),傳入鍵名,有且成功刪除爲true,不然爲false
清空(clear),無需傳參,沒有返回值。

Set獨有的操做方法
新增(add),傳入值,返回實例自己。
沒有相應的獲取方法,由於獲取需傳入的值就是應所傳出的值。

let set = new Set();

set.add(1).add(NaN); // set.size 爲 2。
set.has(NaN); // true
set.delete(NaN); // true
set.has(NaN); // false
set.delete(2); // false
set.clear();

console.log(...set); // 它變得一無全部,只剩一具空殼。

Map獨有的操做方法
新增(set),傳入鍵名和鍵值,有則更新沒則新增,返回實例自己。
獲取(get),傳入鍵名,返回相應值沒有則爲undefined

let o = [1, 2, 3];
let map = new Map();

map.set(o, 1).set(o, 2); // map.size 爲 1
map.has(o); // true
map.get(o); // 2
map.get([1, 2, 3]); // undefined
map.delete(o); // true
map.clear();

console.log(...map); // 它再次一無全部,只剩愈發飢渴的獸心。

3.2 遍歷方法

返回鍵名的遍歷器對象(keys)。
返回鍵值的遍歷器對象(values)。
返回鍵值對的遍歷器對象(entries),鍵值對爲[鍵名, 鍵值]
遍歷每一個成員(forEach),使用方式與Array的方法相同。

由於Set的鍵名和鍵值相同,因此通常只使用values方法獲取所有值。而Map則根據相應需求獲取便可。

let set = new Set([1, 2, 3]);
let map = new Map([[1, 'one'], [null, NaN]]);

[...set.values()]; // [1, 2, 3]
[...set.keys()]; // [1, 2, 3]
[...set.entries()]; // [[1, 1], [2, 2], [3, 3]]

[...map.values()]; // ['one', NaN]
[...map.keys()]; // [1, null]
[...map.entries()]; // [[1, 'one'], [null, NaN]]

二者的forEach方法與數組的不一樣點在於回調函數的第二個參數,前者爲該項的鍵名後者爲該項的序號。

let set = new Set([1, 2, 3]);
let map = new Map([[1, 'one'], [null, NaN]]);

set.forEach((v, i) => console.log(i)); // 1, 2, 3
map.forEach((v, i) => console.log(i)); // 1, null
[1, 2, 3].forEach((v, i) => console.log(i)); // 0, 1, 2

對象屬性的遍歷順序
不一樣遍歷對象的方法面向的數據種類不一樣,但總的說其遍歷順序是這樣的:先找到其中可轉化成數值的屬性並按升序遍歷,再遍歷字符串屬性按加入時間的先後,最後遍歷Symbol值按加入時間的先後。Map的遍歷順序即其被插入時的順序,嗯,總有些色咪咪的味道。

let obj = {
  '0': 0,
  1: 1,
  'b': 2,
  'a': 3,
  [Symbol(2)]: 4,
  [Symbol(1)]: 5
};

Reflect.ownKeys(obj); // ["0", "1", "b", "a", Symbol(2), Symbol(1)]

4 弱引用集合

弱引用集合WeakSetWeakMap是由SetMap分別衍生出的,其與本體的異同點一致,所以只對WeakMap進行說明。

在JS的垃圾回收機制中,對象會在沒有引用時(可意爲使用)被回收。這說明着,若是有個數據集合(數組、對象、SetMap)中包含了某對象(在使用它),那麼在此數據集合被回收以前該對象都不能被回收。這很容易致使內存泄漏(專有名詞,可簡單理解爲內存被沒用的數據佔據)。

弱引用集合的設計目的就是爲了解決這個問題。弱引用顧名思義是指雖然某對象被此集合引用了,但該引用不被引擎保護,不被垃圾回收裝置考慮在內,該回收時就得乖乖的被回收。那些被慣成畸形的傢伙們,要知道,媽媽的懷抱可不是個萬全的地方哦,惟有死神的纔是。

WeakMap的行爲與Map除了如下幾點不一樣外,能夠認爲是一致的。
WeakMap的鍵名只能是對象(不包括null),否者報錯。若是能放入普通類型,那有什麼意義呢?
WeakMap的鍵名是動態不定的,不知道何時會被回收。鍵名指代的對象被回收後,該項會被自動消除。
由於項數的動態性,因此不能被遍歷(沒有遍歷方法),沒有size屬性,沒有cealr方法。

let o = {};
let wm = new WeakMap();

wm.set(1, 1); // 報錯,1 不是對象。
wm.set(o, 1);
wm.has(o); // true
wm.get(o); // 1
wm.delete(o); // true

'size' in wm; // false
'clear' in wm; // false
'values' in wm; // false

弱引用集合的優勢在於,咱們能夠任意爲其註冊對象,而不用擔憂內存泄漏。典型的應用場景是將DOM與數據進行綁定。一些要在DOM中綁定數據的庫中,好比d3,會直接在DOM對象上設置屬性進行保存。但在平常組建單頁面程序中的某個階段,想將DOM與數據聯繫在一塊兒時,咱們顯然會優先選用數據映射的方式。而弱引用集合的出現,更加優化了這種方式。

在下面的示例中,每次點擊請求數據後都會生成幫了相應數據項的li標籤,並將該標籤與相應的數據進行綁定。在這一系列輪迴存儲綁定中,由於WeakMap的弱引用特性,咱們不須要關心已經被刪除的DOM元素。每次只需進行相同的操做,方便安心,省時省力。

<button onclick="requestData()">Request Data</button>

<ul id="container"></ul>

<script>
  "use strict";

  let wmap = new WeakMap();
  let container = document.querySelector('#container');
  
  function requestData() {
    container.innerHTML = '';

    [0, 0, 0].map(() => Math.random()).forEach(d => {
      let li = document.createElement('li');
      li.innerHTML = `
        <button onclick="showMes(this)">Show Message</button>
        <button onclick="deleteItem(this)">Delete Item</button>
      `;
      wmap.set(li, d);
      container.appendChild(li);
    });
  }

  function showMes(that) {
    let li = that.parentNode;
    li.innerHTML = wmap.get(li);
  }
  function deleteItem(that) {
    let li = that.parentNode;
    container.removeChild(li);
  }
</script>

延伸閱讀

ES6精華:Symbol
ES6精華:解構賦值
ES6精華:函數擴展
ES6精華:Proxy & Reflect
Iterator:訪問數據集合的統一接口

相關文章
相關標籤/搜索