JavaScript ES6 Map and WeakMap Object 物件

ES6 新增了 Map 和 WeakMap 數據結構。

Map

Map 有點像是 object (key-value pairs),兩者不同的地方在於 object 的 key 只能是字串 (string);而 Map 的 key 則可以是任何的資料型態!

另外 Map 中的資料是有序的,當你遍歷一個 Map 資料結構時,會依照 key-value pairs 先前寫入的順序 (insertion order)。

建立 Map 物件的語法:

// 建立一個空 Map
new Map()

// 建立一個 Map 並初始化
new Map(iterable)

使用範例:

// 建立一個 Map 物件
var myMap = new Map();

var keyString = 'a string';
var keyObj = {};
var keyFunc = function() {};

// set() 用來新增 key-value pair

// key 不限字串,可以是任何的資料型態
myMap.set(keyString, "value associated with 'a string'");
myMap.set(keyObj, 'value associated with keyObj');
myMap.set(keyFunc, 'value associated with keyFunc');

// get() 用來取得某個 key 的值

// 顯示 "value associated with 'a string'"
console.log( myMap.get(keyString) );
// 顯示 "value associated with keyObj"
console.log( myMap.get(keyObj) );
// 顯示 "value associated with keyFunc"
console.log( myMap.get(keyFunc) );

// 因為 keyString === 'a string'
// 顯示 "value associated with 'a string'"
console.log( myMap.get('a string') );

// 因為 because keyObj !== {}
// 顯示 undefined
console.log( myMap.get({}) );

// 因為 keyFunc !== function () {}
// 顯示 undefined
console.log( myMap.get(function() {}) );

Map 的構造函數 (constructor) 可以傳入一個陣列當作初始化參數,陣列中的元素是有兩個值的陣列用來表示 key-value pairs:

var kvArray = [['key1', 'value1'], ['key2', 'value2']];

var myMap = new Map(kvArray);

// 顯示 value1
console.log( myMap.get('key1') );

// 顯示 Map {"key1" => "value1", "key2" => "value2"}
console.log( myMap );

傳入一個參數,背後其實是幫你執行:

var kvArray = [['key1', 'value1'], ['key2', 'value2']];

var myMap = new Map();

kvArray.forEach(([key, value]) => myMap.set(key, value));

Map 物件的屬性和方法 Properties and Methods

Map.prototype.size

用來取得 Map 物件的大小,共有多少個 key/value pairs。

var map = new Map();

map.set('foo', 1);
map.set('bar', 2);

// 2
map.size;

Map.prototype.set(key, value)

用來新增 key-value pair,如果 key 已經存在,其值會被新值覆蓋過去,set() 方法會返回 Map 本身。

var m = new Map();

m.set('str', 123);
m.set(101, [1, 2, 3]);
m.set(undefined, 'blah');

因為 set() 方法會返回 Map 本身,所以你可以用 chaining 的寫法:

var m = new Map();

m.set('str', 123)
 .set(101, [1, 2, 3])
 .set(undefined, 'blah');

Map.prototype.get(key)

取得某個 key 的值,如果沒有這個 key 則返回 undefined。

var m = new Map([['a', 1], ['b', 2]]);

// 1
m.get('a');

// undefined
m.get('c');

Map.prototype.delete(key)

刪除某個 key,如果刪除成功會返回 true,反之返回 false。

var m = new Map([['a', 1], ['b', 2]]);

// true
m.delete('a');

// false
m.delete('c');

// Map {"b" => 2}
m;

Map.prototype.has(key)

返回一個 boolean,判斷 Map 中有沒某個 key。

var m = new Map([['a', 1], ['b', 2]]);

// true
m.has('b');

// false
m.has('d');

Map.prototype.clear()

清空 Map,刪除全部的 key/value pairs,沒有返回值。

var m = new Map([['a', 1], ['b', 2]]);

// Map {"a" => 1, "b" => 2}
m;

m.clear();

// Map {}
m;

Map.prototype.keys()

返回一個 Iterator 物件表示全部的 keys。

var m = new Map([['a', 1], ['b', 2], ['c', 3]]);

for (let k of m.keys()) {
    console.log(k);
}

// 依序輸出 a b c

Map.prototype.values()

返回一個 Iterator 物件表示全部的 values。

var m = new Map([['a', 1], ['b', 2], ['c', 3]]);

for (let v of m.values()) {
    console.log(v);
}

// 依序輸出 1 2 3

Map.prototype.entries()

返回一個 Iterator 物件,其中每一個值是 [key, value] 結構的陣列。

var m = new Map([['a', 1], ['b', 2], ['c', 3]]);

for (let [k, v] of m.entries()) {
    console.log(`${k}=>${v}`);
}

// 依序輸出 a=>1 b=>2 c=>3

Map.prototype.forEach(callbackFn[, thisArg])

forEach() 方法可以用來遍歷 Map。

其中第一個參數 callbackFn 是一個函數,有三個參數:

  1. value
  2. key
  3. Map 物件本身

第二個參數 thisArg 不是必要的參數,表示 this 指向的物件。

var m = new Map([['a', 1], ['b', 2], ['c', 3]]);

m.forEach(function(value, key, map) {
    console.log(`${key}=>${value}`);
});

// 依序輸出 a=>1 b=>2 c=>3

for...of

也可以用 for...of 來遍歷 Map。

var m = new Map([['a', 1], ['b', 2], ['c', 3]]);

for (let [k, v] of m) {
    console.log(`${k}=>${v}`);
}

// 依序輸出 a=>1 b=>2 c=>3

WeakMap

WeakMap 和 Map 資料結構基本上是類似的,唯一的區別是 WeakMap 只接受 object 當作 key (除了 null 以外,null 也不行當作是 key),基本資料型態 (primitive data types) 都不能被當作是 key 。

例如:

var wm = new WeakMap();

// 錯誤
// TypeError: Invalid value used as weak map key
wm.set('a', 1);

// 錯誤
// TypeError: Invalid value used as weak map key
wm.set(101, 2);

WeakMap 中的 key 所指向的 object 不會被垃圾回收機制 (garbage collection) 計入參考,這也就是 weak 的意思 - 弱引用 (weakly reference)。

因為 weakly reference,所以 WeakMap 中的 object 可能隨時會被自動回收 (garbage collected),而當 object 被回收後,其所對應的 key-value pair 也會自動被刪除。

WeakMap 的典型運用之一是引用 DOM 元素物件,當 DOM 元素被移除後,對應的 WeakMap 紀錄也會自動被移除,所以說用 WeakMap 可以方便地避免內存泄露 (memory leak) 的問題。

WeakMap 的操作基本上和 Map 相似:

const wm1 = new WeakMap(),
      wm2 = new WeakMap(),
      wm3 = new WeakMap();
const o1 = {},
      o2 = function() {},
      o3 = window;

wm1.set(o1, 37);
wm1.set(o2, 'azerty');
wm2.set(o1, o2); // value 可以是任何型態,包含像 object 或 function
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // key 和 value 可以是任何物件,甚至是個 WeakMaps

wm1.get(o2); // "azerty"
wm2.get(o2); // undefined 因為 wm2 沒 o2 這 key
wm2.get(o3); // undefined 因為 o3 key 的 value 是設 undefined

wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true 就算 value 是設 undefined 還是有 o3 這 key 喔

wm3.set(o1, 37);
wm3.get(o1); // 37

wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false

因為 WeakMap weakly reference 的特性,WeakMap 不支援遍歷類型的操作,像是 keys(), values(), entries(), forEach(),也不支援 size 和 clear(),只支援四個方法 set(), get(), has(), delete()。

再舉一個 WeakMap 實用的例子:

let myBtn = document.getElementById('btn');

let wm = new WeakMap();
wm.set(myBtn, {clickCnt: 0});

myBtn.addEventListener('click', function() {

    let btnData = wm.get(myBtn);
    btnData.clickCnt++;

}, false);

上面是一個紀錄 btn 元素被點擊的次數的程式碼,我們把點擊的資料存在一個 WeakMap 中,key 就是對應的 DOM 元素物件,而一但 btn 元素被從 DOM 中移除時,WeakMap 中對應的 key-value pair 也會一起被移除,也就是說不用特別做什麼,就能確保不會造成 memory leak 的風險。