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 是一個函數,有三個參數:
- value
- key
- 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 的風險。