WeakMap 和 Map 的區(qū)別,WeakMap 原理,為什么能被 GC?
垃圾回收機制
我們知道,程序運行中會有一些垃圾數據不再使用,需要及時釋放出去,如果我們沒有及時釋放,這就是內存泄露
JS 中的垃圾數據都是由垃圾回收(Garbage Collection,縮寫為 GC)器自動回收的,不需要手動釋放,它是如何做的喃?
很簡單,JS 引擎中有一個后臺進程稱為垃圾回收器,它監(jiān)視所有對象,觀察對象是否可被訪問,然后按照固定的時間間隔周期性的刪除掉那些不可訪問的對象即可
現在各大瀏覽器通常用采用的垃圾回收有兩種方法:
引用計數 標記清除
引用計數
最早最簡單的垃圾回收機制,就是給一個占用物理空間的對象附加一個引用計數器,當有其它對象引用這個對象時,這個對象的引用計數加一,反之解除時就減一,當該對象引用計數為 0 時就會被回收。
該方式很簡單,但會引起內存泄漏:
// 循環(huán)引用的問題
function temp(){
var a={};
var b={};
a.o = b;
b.o = a;
}
這種情況下每次調用 temp 函數,a 和 b 的引用計數都是 2 ,會使這部分內存永遠不會被釋放,即內存泄漏。現在已經很少使用了,只有低版本的 IE 使用這種方式。
標記清除
V8 中主垃圾回收器就采用標記清除法進行垃圾回收。主要流程如下:
標記:遍歷調用棧,看老生代區(qū)域堆中的對象是否被引用,被引用的對象標記為活動對象,沒有被引用的對象(待清理)標記為垃圾數據。 垃圾清理:將所有垃圾數據清理掉

(圖片來源:How JavaScript works: memory management + how to handle 4 common memory leaks)
在我們的開發(fā)過程中,如果我們想要讓垃圾回收器回收某一對象,就將對象的引用直接設置為 null
var a = {}; // {} 可訪問,a 是其引用
a = null; // 引用設置為 null
// {} 將會被從內存里清理出去
但如果一個對象被多次引用時,例如作為另一對象的鍵、值或子元素時,將該對象引用設置為 null 時,該對象是不會被回收的,依然存在
var a = {};
var arr = [a];
a = null;
console.log(arr)
// [{}]
如果作為 Map 的鍵喃?
var a = {};
var map = new Map();
map.set(a, '三分鐘學前端')
a = null;
console.log(map.keys()) // MapIterator {{}}
console.log(map.values()) // MapIterator {"三分鐘學前端"}
如果想讓 a 置為 null 時,該對象被回收,該怎么做喃?
WeakMap vs Map
ES6 考慮到了這一點,推出了:WeakMap 。它對于值的引用都是不計入垃圾回收機制的,所以名字里面才會有一個"Weak",表示這是弱引用(對對象的弱引用是指當該對象應該被GC回收時不會阻止GC的回收行為)。
Map 相對于 WeakMap :
Map的鍵可以是任意類型,WeakMap只接受對象作為鍵(null除外),不接受其他類型的值作為鍵Map的鍵實際上是跟內存地址綁定的,只要內存地址不一樣,就視為兩個鍵;WeakMap的鍵是弱引用,鍵所指向的對象可以被垃圾回收,此時鍵是無效的Map可以被遍歷,WeakMap不能被遍歷
下面以 WeakMap 為例,看看它是怎么上面問題的:
var a = {};
var map = new WeakMap();
map.set(a, '三分鐘學前端')
map.get(a)
a = null;
上例并不能看出什么?我們通過 process.memoryUsage 測試一下:
//map.js
global.gc(); // 0 每次查詢內存都先執(zhí)行gc()再memoryUsage(),是為了確保垃圾回收,保證獲取的內存使用狀態(tài)準確
function usedSize() {
const used = process.memoryUsage().heapUsed;
return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
}
console.log(usedSize()); // 1 初始狀態(tài),執(zhí)行gc()和memoryUsage()以后,heapUsed 值為 1.64M
var map = new Map();
var b = new Array(5 * 1024 * 1024);
map.set(b, 1);
global.gc();
console.log(usedSize()); // 2 在 Map 中加入元素b,為一個 5*1024*1024 的數組后,heapUsed為41.82M左右
b = null;
global.gc();
console.log(usedSize()); // 3 將b置為空以后,heapUsed 仍為41.82M,說明Map中的那個長度為5*1024*1024的數組依然存在
執(zhí)行 node --expose-gc map.js 命令:

其中,--expose-gc 參數表示允許手動執(zhí)行垃圾回收機制
// weakmap.js
function usedSize() {
const used = process.memoryUsage().heapUsed;
return Math.round((used / 1024 / 1024) * 100) / 100 + "M";
}
global.gc(); // 0 每次查詢內存都先執(zhí)行gc()再memoryUsage(),是為了確保垃圾回收,保證獲取的內存使用狀態(tài)準確
console.log(usedSize()); // 1 初始狀態(tài),執(zhí)行gc()和 memoryUsage()以后,heapUsed 值為 1.64M
var map = new WeakMap();
var b = new Array(5 * 1024 * 1024);
map.set(b, 1);
global.gc();
console.log(usedSize()); // 2 在 Map 中加入元素b,為一個 5*1024*1024 的數組后,heapUsed為41.82M左右
b = null;
global.gc();
console.log(usedSize()); // 3 將b置為空以后,heapUsed 變成了1.82M左右,說明WeakMap中的那個長度為5*1024*1024的數組被銷毀了
執(zhí)行 node --expose-gc weakmap.js 命令:

上面代碼中,只要外部的引用消失,WeakMap 內部的引用,就會自動被垃圾回收清除。由此可見,有了它的幫助,解決內存泄漏就會簡單很多。
最后看一下 WeakMap
WeakMap
WeakMap 對象是一組鍵值對的集合,其中的鍵是弱引用對象,而值可以是任意。
注意,WeakMap 弱引用的只是鍵名,而不是鍵值。鍵值依然是正常引用。
WeakMap 中,每個鍵對自己所引用對象的引用都是弱引用,在沒有其他引用和該鍵引用同一對象,這個對象將會被垃圾回收(相應的key則變成無效的),所以,WeakMap 的 key 是不可枚舉的。
屬性:
constructor:構造函數
方法:
has(key):判斷是否有 key 關聯對象 get(key):返回key關聯對象(沒有則則返回 undefined) set(key):設置一組key關聯對象 delete(key):移除 key 的關聯對象
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
myWeakmap.set(myElement, {timesClicked: 0});
myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
}, false);
除了 WeakMap 還有 WeakSet 都是弱引用,可以被垃圾回收機制回收,可以用來保存DOM節(jié)點,不容易造成內存泄漏
另外還有 ES12 的 WeakRef ,感興趣的可以了解下,今晚太晚了,之后更新
參考
你不知道的 WeakMap
來自:https://github.com/Advanced-Frontend/Daily-Interview-Question
