ES6 系列之 WeakMap

冴羽發表於2018-07-26

前言

我們先從 WeakMap 的特性說起,然後聊聊 WeakMap 的一些應用場景。

特性

1. WeakMap 只接受物件作為鍵名

const map = new WeakMap();
map.set(1, 2);
// TypeError: Invalid value used as weak map key
map.set(null, 2);
// TypeError: Invalid value used as weak map key
複製程式碼

2. WeakMap 的鍵名所引用的物件是弱引用

這句話其實讓我非常費解,我個人覺得這句話真正想表達的意思應該是:

WeakMaps hold "weak" references to key objects,

翻譯過來應該是 WeakMaps 保持了對鍵名所引用的物件的弱引用。

我們先聊聊弱引用:

在計算機程式設計中,弱引用與強引用相對,是指不能確保其引用的物件不會被垃圾回收器回收的引用。 一個物件若只被弱引用所引用,則被認為是不可訪問(或弱可訪問)的,並因此可能在任何時刻被回收。

在 JavaScript 中,一般我們建立一個物件,都是建立一個強引用:

var obj = new Object();
複製程式碼

只有當我們手動設定 obj = null 的時候,才有可能回收 obj 所引用的物件。

而如果我們能建立一個弱引用的物件:

// 假設可以這樣建立一個
var obj = new WeakObject();
複製程式碼

我們什麼都不用做,只用靜靜的等待垃圾回收機制執行,obj 所引用的物件就會被回收。

我們再來看看這句:

WeakMaps 保持了對鍵名所引用的物件的弱引用

正常情況下,我們舉個例子:

const key = new Array(5 * 1024 * 1024);
const arr = [
  [key, 1]
];
複製程式碼

使用這種方式,我們其實建立了 arr 對 key 所引用的物件(我們假設這個真正的物件叫 Obj)的強引用。

所以當你設定 key = null 時,只是去掉了 key 對 Obj 的強引用,並沒有去除 arr 對 Obj 的強引用,所以 Obj 還是不會被回收掉。

Map 型別也是類似:

let map = new Map();
let key = new Array(5 * 1024 * 1024);

// 建立了 map 對 key 所引用物件的強引用
map.set(key, 1);
// key = null 不會導致 key 的原引用物件被回收
key = null;
複製程式碼

我們可以通過 Node 來證明一下這個問題:

// 允許手動執行垃圾回收機制
node --expose-gc

global.gc();
// 返回 Nodejs 的記憶體佔用情況,單位是 bytes
process.memoryUsage(); // heapUsed: 4640360 ≈ 4.4M

let map = new Map();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);
global.gc();
process.memoryUsage(); // heapUsed: 46751472 注意這裡大約是 44.6M

key = null;
global.gc();
process.memoryUsage(); // heapUsed: 46754648 ≈ 44.6M

// 這句話其實是無用的,因為 key 已經是 null 了
map.delete(key);
global.gc();
process.memoryUsage(); // heapUsed: 46755856 ≈ 44.6M
複製程式碼

如果你想要讓 Obj 被回收掉,你需要先 delete(key) 然後再 key = null:

let map = new Map();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);
map.delete(key);
key = null;
複製程式碼

我們依然通過 Node 證明一下:

node --expose-gc

global.gc();
process.memoryUsage(); // heapUsed: 4638376 ≈ 4.4M

let map = new Map();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);
global.gc();
process.memoryUsage(); // heapUsed: 46727816 ≈ 44.6M

map.delete(key);
global.gc();
process.memoryUsage(); // heapUsed: 46748352 ≈ 44.6M

key = null;
global.gc();
process.memoryUsage(); // heapUsed: 4808064 ≈ 4.6M
複製程式碼

這個時候就要說到 WeakMap 了:

const wm = new WeakMap();
let key = new Array(5 * 1024 * 1024);
wm.set(key, 1);
key = null;
複製程式碼

當我們設定 wm.set(key, 1) 時,其實建立了 wm 對 key 所引用的物件的弱引用,但因為 let key = new Array(5 * 1024 * 1024) 建立了 key 對所引用物件的強引用,被引用的物件並不會被回收,但是當我們設定 key = null 的時候,就只有 wm 對所引用物件的弱引用,下次垃圾回收機制執行的時候,該引用物件就會被回收掉。

我們用 Node 證明一下:

node --expose-gc

global.gc();
process.memoryUsage(); // heapUsed: 4638992 ≈ 4.4M

const wm = new WeakMap();
let key = new Array(5 * 1024 * 1024);
wm.set(key, 1);
global.gc();
process.memoryUsage(); // heapUsed: 46776176 ≈ 44.6M

key = null;
global.gc();
process.memoryUsage(); // heapUsed: 4800792 ≈ 4.6M
複製程式碼

所以 WeakMap 可以幫你省掉手動刪除物件關聯資料的步驟,所以當你不能或者不想控制關聯資料的生命週期時就可以考慮使用 WeakMap。

總結這個弱引用的特性,就是 WeakMaps 保持了對鍵名所引用的物件的弱引用,即垃圾回收機制不將該引用考慮在內。只要所引用的物件的其他引用都被清除,垃圾回收機制就會釋放該物件所佔用的記憶體。也就是說,一旦不再需要,WeakMap 裡面的鍵名物件和所對應的鍵值對會自動消失,不用手動刪除引用。

也正是因為這樣的特性,WeakMap 內部有多少個成員,取決於垃圾回收機制有沒有執行,執行前後很可能成員個數是不一樣的,而垃圾回收機制何時執行是不可預測的,因此 ES6 規定 WeakMap 不可遍歷。

所以 WeakMap 不像 Map,一是沒有遍歷操作(即沒有keys()、values()和entries()方法),也沒有 size 屬性,也不支援 clear 方法,所以 WeakMap只有四個方法可用:get()、set()、has()、delete()。

應用

1. 在 DOM 物件上儲存相關資料

傳統使用 jQuery 的時候,我們會通過 $.data() 方法在 DOM 物件上儲存相關資訊(就比如在刪除按鈕元素上儲存帖子的 ID 資訊),jQuery 內部會使用一個物件管理 DOM 和對應的資料,當你將 DOM 元素刪除,DOM 物件置為空的時候,相關聯的資料並不會被刪除,你必須手動執行 $.removeData() 方法才能刪除掉相關聯的資料,WeakMap 就可以簡化這一操作:

let wm = new WeakMap(), element = document.querySelector(".element");
wm.set(element, "data");

let value = wm.get(elemet);
console.log(value); // data

element.parentNode.removeChild(element);
element = null;
複製程式碼

2. 資料快取

從上一個例子,我們也可以看出,當我們需要關聯物件和資料,比如在不修改原有物件的情況下儲存某些屬性或者根據物件儲存一些計算的值等,而又不想管理這些資料的死活時非常適合考慮使用 WeakMap。資料快取就是一個非常好的例子:

const cache = new WeakMap();
function countOwnKeys(obj) {
    if (cache.has(obj)) {
        console.log('Cached');
        return cache.get(obj);
    } else {
        console.log('Computed');
        const count = Object.keys(obj).length;
        cache.set(obj, count);
        return count;
    }
}
複製程式碼

3. 私有屬性

WeakMap 也可以被用於實現私有變數,不過在 ES6 中實現私有變數的方式有很多種,這只是其中一種:

const privateData = new WeakMap();

class Person {
    constructor(name, age) {
        privateData.set(this, { name: name, age: age });
    }

    getName() {
        return privateData.get(this).name;
    }

    getAge() {
        return privateData.get(this).age;
    }
}

export default Person;
複製程式碼

ES6 系列

ES6 系列目錄地址:github.com/mqyqingfeng…

ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級作用域、標籤模板、箭頭函式、Symbol、Set、Map 以及 Promise 的模擬實現、模組載入方案、非同步處理等內容。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。

相關文章