如何理解WeakMap?

如何理解WeakMap?.png

在學習緩存函數時,最後提到了WeakMap方式緩存(對入參類型爲對象作緩存,而且當對象在WeakMap中的key沒有引用時方便瀏覽器垃圾回收)html

If our parameter were an object (rather than a string, like it is above), we could use WeakMap instead of Map in modern browsers. The benefit of WeakMap is that it would automatically 「clean up」 the entries when our object key is no longer accessible.

並且JavaScript既然已經有了Map類型的數據結構,爲何還有一種叫作WeakMap類型的數據結構呢?它和垃圾回收有什麼關係?react

WeakMap很早以前就遇到過,可是沒有系統學習過,今天就來對它一探究竟。git

  • 初識WeakMapgithub

    • WeakMap的key爲何是弱引用的?
    • WeakMap與Map最大的不一樣
  • 新增WeakMap類型是爲何?
  • WeakMap的實例方法
  • WeakMap最簡使用方式
  • WeakMap存儲私有數據
  • 擁有clear方式的WeakMap類
  • WeakMap式自動垃圾回收緩存函數
  • 參考資料

初識WeakMap

  • WeakMap對象是一組鍵值對的集合,其中key是弱引用的
  • WeakMap的key必須是對象類型,value能夠是任意類型

WeakMap的key爲何是弱引用的?

弱引用的意義:若是是做爲key的對象沒有任何地方引用它的話,垃圾收集器(GC)會將其標記爲目標而且進行垃圾回收數組

WeakMap的key和value能夠是哪些類型

key:必須是任意object類型(對象、數組、Map、WeakMap等等)
value:any(任意類型,因此也包括undefined,null)瀏覽器

WeakMap與Map最大的不一樣

WeakMap的key是不可枚舉的,而Map是可枚舉的。
不可枚舉就意味着獲取不到WeakMap的key列表。緩存

設計爲不可枚舉的緣由是由於:若是枚舉WeakMap的key,那就須要依賴垃圾回收器(GC)的狀態,從而引入不肯定性。數據結構

新增WeakMap類型是爲何?

map API在js中能夠經過共享4個API(get,set,has,delete)的兩個數組來實現:一個存儲key,一個存儲value。在這個map上設置元素同步推入一個key和一個value到數組尾部。做爲結果,key和value的索引會和兩個數組綁定起來。從map獲取一個值得話,會遍歷全部key去找到一個匹配的,而後使用這個匹配到的index從values數組中查詢到對應的值。ide

這樣實現的話會有2個主要的弊端:函數

  1. 首先是set和search的時間複雜度是O(n),n是map中key數組的數量,由於都須要遍歷列表去查找到須要的值
  2. 其次是會形成內存泄漏,由於數組須要無期限地去確保每一個key和每一個value的引用。這些引用會致使阻止key被垃圾回收掉,即便這個對象沒有任何地方再引用到了,key對應的value也一樣會被阻止垃圾回收。

相比之下,原生的WeakMap會保持對key的「弱」引用。原生的WeakMap不會阻止垃圾回收,最終會移除對key對象的引用。「弱」引用一樣可讓value很好地垃圾回收。WeakMap特別適用於key映射的信息只有不被垃圾回收時纔有價值的場景,換句話說就是WeakMap適用於動態垃圾回收key的場景。

由於引用是弱的,因此WeakMap的鍵是不能枚舉的。沒有方法去獲取key的列表。若是枚舉WeakMap的key,那就須要依賴垃圾回收器(GC)的狀態,從而引入不肯定性。若是必需要有key的話,應該去使用Map

WeakMap的基本概念

語法

new WeakMap()
new WeakMap(iterable)

其中iterable是數組或者任意能夠迭代的對象,須要擁有key-value對(通常是一個二維數組)。null會被當作undefined。

iterable爲二維數組
const iterable = [
    [{foo:1}, 1], 
    [[1,2,3], 2], 
    [window, 3]
]
const iwm = new WeakMap(iterable)
// WeakMap {{…} => 1, Window => 3, Array(3) => 2}

實例方法

WeakMap.prototype.delete(key)

刪除key關聯的任意值。刪除後WeakMap.prototype.has(key)返回false。

WeakMap.prototype.get(key)

返回與key關聯的值,假設不存在關聯值得話返回undefined。

WeakMap.prototype.has(key)

返回key在WeakMap上是否存在的結果。

WeakMap.prototype.set(key, value)

在WeakMap對象上爲對應key設置指定的value。而且返回WeakMap對象

WeakMap最簡使用方式

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); // WeakMap的值能夠是任意類型,包括object和function
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // key和value能夠是任意對象。包括WeakMap!

wm1.get(o2); // "azerty"
wm2.get(o2); // undefined, wm2上沒有o2這個key
wm2.get(o3); // undefined, 由於這是設置的值

wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (即便value是undefined)

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

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

WeakMap存儲私有數據

實例和原型鏈上的數據和方法是公開的,因此能夠經過WeakMap類型的私有變量去隱藏實現細節。

const privates = new WeakMap();

function Public() {
  const me = {
    // Private data goes here
  };
  privates.set(this, me);
}

Public.prototype.method = function () {
  const me = privates.get(this);
  // Do stuff with private data in `me`...
};

module.exports = Public;

擁有clear方式的WeakMap類

class ClearableWeakMap {
  constructor(init) {
    this._wm = new WeakMap(init);
  }
  clear() {
    this._wm = new WeakMap();
  }
  delete(k) {
    return this._wm.delete(k);
  }
  get(k) {
    return this._wm.get(k);
  }
  has(k) {
    return this._wm.has(k);
  }
  set(k, v) {
    this._wm.set(k, v);
    return this;
  }
}
const key1 = {foo:1};
const key2 = [1,2,3];
const key3 = window;
const cwm = new ClearableWeakMap([
    [key1, 1], 
    [key2, 2], 
    [key3, 3]
])
cwm.has(key1) // true
console.log(cwm);// ClearableWeakMap {_wm: WeakMap {Window => 3, {…} => 1, Array(3) => 2}}
cwm.clear(); // 垃圾回收當前WeakMap,而且聲稱新的空WeakMap
cwm.has(key1) // false
console.log(cwm);// ClearableWeakMap {_wm: WeakMap {}}

WeakMap式自動垃圾回收緩存函數

實現緩存函數的方式有不少種,好比單次緩存,Map式全量緩存,LRU最近最少緩存等等。
那麼爲何還須要WeakMap式的緩存函數呢?這是由於入參爲對象類型的緩存且方便瀏覽器垃圾回收。

緩存函數實現

function memoizeWeakMap(fn) {
  const wm = new WeakMap();
  return function (arg) {
    if (wm.has(arg)) {
      return wm.get(arg);
    }
    const cachedArg = arg;
    const cachedResult = fn(arg);
    wm.set(cachedArg, cachedResult)
    console.log('weakmap object', wm)
    return cachedResult;
  };
}

let testFn = (bar) => {return Object.prototype.toString.call(bar)}; // 這裏須要改造一下,改造完返回傳入對象的類型

let memoizeWeakMapFn = memoizeWeakMap(testFn);

memoizeWeakMapFn(document) // weakmap對document生成緩存
memoizeWeakMapFn([1,2,3]) // weakmap對[1,2,3]生成緩存
memoizeWeakMapFn(function(){}) // weakmap對function(){}生成緩存

memoizeWeakMapFn(new WeakMap())  // weakmap對WeakMap實例生成緩存
memoizeWeakMapFn(new Map()) // weakmap對Map實例生成緩存
memoizeWeakMapFn(new Set())  // weakmap對Set實例生成緩存

WeakMap:
0: {Array(3) => "[object Array]"}
1: {function(){} => "[object Function]"}
2: {WeakMap => "[object WeakMap]"}
3: {Map(0) => "[object Map]"}
4: {#document => "[object HTMLDocument]"}
5: {Set(0) => "[object Set]"}

如何體現出WeakMap的垃圾回收特性呢

// 忽略部分代碼同上
setTimeout(()=>{
    memoizeWeakMapFn(document)    
},5000)

此時有時最後一次weakmap的打印結果以下:

WeakMap:
0: {#document => "[object HTMLDocument]"}
爲何說是「有時」?

由於打印時垃圾回收可能並無執行完成,雖然會帶來不肯定性,可是能夠肯定的是,假設對象沒有再被引用,WeakMap中的key會被瀏覽器自動垃圾回收掉。

爲何weakmap中僅保存了document?

這是由於[1,2,3], function(){},new WeakMap(),new Map(),new Set()在後面都沒有再繼續引用了,並且由於它們做爲了WeakMap的key,因此會被瀏覽器自動垃圾回收掉。

如何不讓key被垃圾回收掉呢?

保持一個變量對它的引用。

let memoizeWeakMapFn = memoizeWeakMap(testFn);
let retainArray = [1,2,3]; // 保持引用避免被垃圾回收
let retainMap = new Map(); // 保持引用避免被垃圾回收

memoizeWeakMapFn(document) // weakmap對document生成緩存
memoizeWeakMapFn(retainArray) // weakmap對[1,2,3]生成緩存
memoizeWeakMapFn(function(){}) // weakmap對function(){}生成緩存

memoizeWeakMapFn(new WeakMap())  // weakmap對WeakMap實例生成緩存
memoizeWeakMapFn(retainMap) // weakmap對Map實例生成緩存
memoizeWeakMapFn(new Set())  // weakmap對Set實例生成緩存

setTimeout(()=>{
    memoizeWeakMapFn(document)    
},5000)

此時打印結果爲:

WeakMap:
0: {#document => "[object HTMLDocument]"}
1: {Map(0) => "[object Map]"}
2: {Array(3) => "[object Array]"}

這是由於[1,2,3], new Map()被變量retainArray和retainMap持續引用着,因此不會被垃圾回收。而function(){},new WeakMap(),new Set()都沒有再繼續引用了,並且由於它們做爲了WeakMap的key,因此會被瀏覽器自動垃圾回收掉。

若是手動觸發垃圾回收呢?

能夠藉助Chrome DevTools的memory面板工具,有一個手動觸發垃圾回收的按鈕。
image

// ...
setTimeout(()=>{
    memoizeWeakMapFn(document)    
},5000)

好比在上面的例子中,設置了一個5秒的延時:只要代碼運行後的5秒內,去手動觸發「垃圾回收按鈕」,就能夠很精確地看到WeakMap的key被垃圾回收了。

固然5秒這個時間是能夠人爲調整的,保證本身能在setTimeout內的代碼運行前觸發對WeakMap的垃圾回收便可,能夠適當調大。

參考資料:
https://developer.mozilla.org...
https://developer.mozilla.org...
https://fitzgeraldnick.com/20...
https://whatthefuck.is/memoiz...
https://github.com/reactjs/re...

相關文章
相關標籤/搜索