es6 Map和WeakMap

Map

JavaScript 的對象(Object),本質上是鍵值對的集合(Hash 結構),可是傳統上只能用字符串看成鍵。這給它的使用帶來了很大的限制。
爲了解決這個問題,ES6 提供了 Map 數據結構。它相似於對象,也是鍵值對的集合,可是「鍵」的範圍不限於字符串,各類類型的值(包括對象)均可以看成鍵。也就是說,Object 結構提供了「字符串—值」的對應,Map 結構提供了「值—值」的對應,是一種更完善的 Hash 結構實現。若是你須要「鍵值對」的數據結構,Map 比 Object 更合適。算法

Map的構造

ES6 的 Map 類型是鍵值對的有序列表,而鍵和值均可以是任意類型。 鍵的比較使用的是Object.is() ,所以你能將 5 與 "5"
同時做爲鍵,由於它們類型不一樣。這與使用對象屬性做爲鍵的方式(指的是用對象來模擬 Map )大相徑庭,由於對象的屬性會被強制轉換爲字符串。

set方法構造

你能夠調用 set() 方法並給它傳遞一個鍵與一個關聯的值,來給 Map 添加項;此後使用鍵名來調用 get() 方法便能提取對應的值。例如:json

let map = new Map();
map.set("title", "Understanding ES6");
map.set("year", 2016);
console.log(map.get("title")); // "Understanding ES6"
console.log(map.get("year")); // 2016

數組構造

依然與 Set 相似,你能將數組傳遞給 Map 構造器,以便使用數據來初始化一個 Map 。該數組中的每一項也必須是數組,內部數組的首個項會做爲鍵,第二項則爲對應值。所以整個Map 就被這些雙項數組所填充。例如:數組

let map = new Map([["name", "Nicholas"], ["age", 25]]);
console.log(map.has("name")); // true
console.log(map.get("name")); // "Nicholas"
console.log(map.size); // 2

經過構造器中的初始化, "name" 與 "age" 這兩個鍵就被添加到 map 變量中。雖然由數組構成的數組看起來有點奇怪,這對於準確表示鍵來講倒是必要的:由於鍵容許是任意數據類型,將鍵存儲在數組中,是確保它們在被添加到 Map 以前不會被強制轉換爲其餘類型的惟一方法。
Map構造函數接受數組做爲參數,實際上執行的是下面的算法。瀏覽器

const items = [
["name", "Nicholas"]
  ["age", 25] ]
  
  const map = new Map();
  
  items.forEach(
    ([key, value]) => map.set(key, value)
  );

事實上,不只僅是數組,任何具備 Iterator 接口、且每一個成員都是一個雙元素的數組的數據結構均可以當作Map構造函數的參數。這就是說,Set和Map均可以用來生成新的Map緩存

其餘構造

const set = new Set([
      ['foo', 1],
      ['bar', 2]
    ]);
const m1 = new Map(set);
m1.get('foo') // 1

const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3

上面代碼中,咱們分別使用 Set 對象和 Map 對象,看成Map構造函數的參數,結果都生成了新的 Map 對象。數據結構

Map的屬性和方法

一、屬性

Map 一樣擁有 size 屬性,用於指明包含了多少個鍵值對函數

二、方法

2.一、操做方法

2.十一、set(key, value)

set方法設置鍵名key對應的鍵值爲value,而後返回整個 Map 結構。若是key已經有值,則鍵值會被更新,不然就新生成該鍵。優化

const m = new Map();
m.set('edition', 6)        // 鍵是字符串
m.set(262, 'standard')     // 鍵是數值
m.set(undefined, 'nah')    // 鍵是 undefined

set方法返回的是當前的Map對象,所以能夠採用鏈式寫法。設計

let map = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');
2.十二、get(key)

get方法讀取key對應的鍵值,若是找不到key,返回undefined。code

const m = new Map();
const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // 鍵是函數

m.get(hello)  // Hello ES6!

2.1三、has、delete、clear

Map 與 Set 共享了幾個方法,這是有意的,容許你使用類似的方式來與 Map 及 Set 進行交互。如下三個方法在 Map 與 Set
上都存在:
2.13.一、has(key)

has方法返回一個布爾值,表示某個鍵是否在當前 Map 對象之中。

const m = new Map();
m.set('edition', 6);
m.set(262, 'standard');
m.set(undefined, 'nah');
m.has('edition')     // true
m.has('years')       // false
m.has(262)           // true
m.has(undefined)     // true
2.13.二、delete(key)

delete方法刪除某個鍵,返回true。若是刪除失敗,返回false。

const m = new Map();
m.set(undefined, 'nah');
m.has(undefined)     // true

m.delete(undefined)
m.has(undefined)       // false
2.13.四、clear()

clear方法清除全部成員,沒有返回值。

let map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2
map.clear()
map.size // 0

2.二、遍歷方法

2.2.1遍歷器生成函數

• keys():返回鍵名的遍歷器。
• values():返回鍵值的遍歷器。
• entries():返回全部成員的遍歷器

須要特別注意的是,Map 的遍歷順序就是插入順序。

const map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同於使用map.entries()
for (let [key, value] of map) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

上面代碼最後的那個例子,表示 Map 結構的默認遍歷器接口(Symbol.iterator屬性),就是entries方法。

2.2.二、forEach

Map 的 forEach() 方法相似於 Set 與數組的同名方法,它接受一個能接收三個參數的回調函數:

  1. Map 中下個位置的值;
  2. 該值所對應的鍵;
  3. 目標 Map 自身。

回調函數的這些參數更緊密契合了數組 forEach() 方法的行爲,即:第一個參數是值、第二個參數則是鍵(數組中的鍵是數值索引)。此處有個示例:

let map = new Map([
  ["name", "Nicholas"], 
  ["age", 25]
]);
map.forEach(function(value, key, ownerMap) {
  console.log(key + " " + value);
  console.log(ownerMap === map);
});

forEach() 的回調函數輸出了傳給它的信息。其中 value 與 key 被直接輸出, ownerMap
與 map 進行了比較,說明它們是相等的。這就輸出了:

name Nicholas
true
age 25
true

Map與其餘數據結構的相互裝換

Map轉數組:

Map 結構轉爲數組結構,比較快速的方法是使用擴展運算符(...)。

const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]

數組轉爲 Map

將數組傳入 Map 構造函數,就能夠轉爲 Map。

new Map([
  [true, 7],
  [{foo: 3}, ['abc']]
])
// Map {
//   true => 7,
//   Object {foo: 3} => ['abc']
// }

Map 轉爲對象

若是全部 Map 的鍵都是字符串,它能夠無損地轉爲對象。

function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}

const myMap = new Map()
  .set('yes', true)
  .set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }

若是有非字符串的鍵名,那麼這個鍵名會被轉成字符串,再做爲對象的鍵名。

對象轉爲 Map

function objToStrMap(obj) {
  let strMap = new Map();
  for (let k of Object.keys(obj)) {
    strMap.set(k, obj[k]);
  }
  return strMap;
}

objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}

Map 轉爲 JSON

Map 轉爲 JSON 要區分兩種狀況。一種狀況是,Map 的鍵名都是字符串,這時能夠選擇轉爲對象 JSON。

function strMapToJson(strMap) {
  return JSON.stringify(strMapToObj(strMap));
}

let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'

另外一種狀況是,Map 的鍵名有非字符串,這時能夠選擇轉爲數組 JSON。

function mapToArrayJson(map) {
  return JSON.stringify([...map]);
}

let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'

JSON 轉爲 Map

JSON 轉爲 Map,正常狀況下,全部鍵名都是字符串。

function jsonToStrMap(jsonStr) {
  return objToStrMap(JSON.parse(jsonStr));
}

jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}

可是,有一種特殊狀況,整個 JSON 就是一個數組,且每一個數組成員自己,又是一個有兩個成員的數組。這時,它能夠一一對應地轉爲 Map。這每每是 Map 轉爲數組 JSON 的逆操做。

function jsonToMap(jsonStr) {
      return new Map(JSON.parse(jsonStr));
    }

jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}

WeakMap:

WeakMap的特性

WeakMap與Map的區別有兩點。
首先,WeakMap只接受對象做爲鍵名(null除外),不接受其餘類型的值做爲鍵名。

const map = new WeakMap();
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key

其次,WeakMap的鍵名所指向的對象,不計入垃圾回收機制。
WeakMap的設計目的在於,有時咱們想在某個對象上面存放一些數據,可是這會造成對於這個對象的引用。請看下面的例子。

const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
  [e1, 'foo 元素'],
  [e2, 'bar 元素'],
];

上面代碼中,e1和e2是兩個對象,咱們經過arr數組對這兩個對象添加一些文字說明。這就造成了arr對e1和e2的引用。
一旦再也不須要這兩個對象,咱們就必須手動刪除這個引用,不然垃圾回收機制就不會釋放e1和e2佔用的內存。
// 不須要 e1 和 e2 的時候
// 必須手動刪除引用

arr [0] = null;
arr [1] = null;

上面這樣的寫法顯然很不方便。一旦忘了寫,就會形成內存泄露。

WeakMap 就是爲了解決這個問題而誕生的,它的鍵名所引用的對象都是弱引用,即垃圾回收機制不將該引用考慮在內。所以,只要所引用的對象的其餘引用都被清除,垃圾回收機制就會釋放該對象所佔用的內存。也就是說,一旦再也不須要,WeakMap 裏面的鍵名對象和所對應的鍵值對會自動消失,不用手動刪除引用。

WeakMap的構造:

ES6 的 WeakMap 類型是鍵值對的無序列表,其中鍵必須是非空的對象,值則容許是任意類型。 WeakMap 的接口與 Map 的很是類似
// WeakMap 可使用 set 方法添加成員

const wm1 = new WeakMap();
const key = {foo: 1};
wm1.set(key, 2);
wm1.get(key) // 2

// WeakMap 也能夠接受一個數組,
// 做爲構造函數的參數

const k1 = [1, 2, 3];
const k2 = [4, 5, 6];
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
wm2.get(k2) // "bar"

WeakMap的屬性和方法:

WeakMap 與 Map 在 API 上的區別主要是兩個,一是沒有遍歷操做(即沒有keys()、values()和entries()方法),也沒有size屬性。由於沒有辦法列出全部鍵名,某個鍵名是否存在徹底不可預測,跟垃圾回收機制是否運行相關。這一刻能夠取到鍵名,下一刻垃圾回收機制忽然運行了,這個鍵名就沒了,爲了防止出現不肯定性,就統一規定不能取到鍵名。二是沒法清空,即不支持clear方法。所以,WeakMap只有四個方法可用:get()、set()、has()、delete()。

const wm = new WeakMap();
// size、forEach、clear 方法都不存在
wm.size // undefined
wm.forEach // undefined
wm.clear // undefined

WeakMap經常使用場景

Weak Map 的最佳用武之地,就是在瀏覽器中建立一個關聯到特定 DOM 元素的對象。例如,某些用在網頁上的 JS 庫會維護一個自定義對象,用於引用該庫所使用的每個 DOM 元素,而且其映射關係會存儲在內部的對象緩存中。
該方法的困難之處在於:如何判斷一個 DOM 元素已不復存在於網頁中,以便該庫能移除此元素的關聯對象。若作不到,該庫就會繼續保持對 DOM 元素的一個無效引用,並形成內存泄漏。使用 Weak Map 來追蹤 DOM 元素,依然容許將自定義對象關聯到每一個 DOM 元素,而在此對象所關聯的 DOM 元素不復存在時,它就會在 Weak Map 中被自動銷燬。
必須注意的是, Weak Map 的鍵纔是弱引用,而值不是。在 Weak Map 的值中存儲對象會阻止垃圾回收,即便該對象的其餘引用已全都被移除。

當決定是要使用 Weak Map 仍是使用正規 Map 時,首要考慮因素在於你是否只想使用對象類型的鍵。若是你打算這麼作,那麼最好的選擇就是 Weak Map 。由於它能確保額外數據在再也不可用後被銷燬,從而能優化內存使用並規避內存泄漏。 要記住 Weak Map 只爲它們的內容提供了很小的可見度,所以你不能使用 forEach() 方法、size 屬性或 clear() 方法來管理其中的項。若是你確實須要一些檢測功能,那麼正規 Map會是更好的選擇,只是必定要確保留意內存的使用。
相關文章
相關標籤/搜索