Javascript WeakMap and WeakSet(弱映射和弱集合)

2023-02-17 10:49 更新

我們從前面的 垃圾回收 章節(jié)中知道,JavaScript 引擎在值“可達”和可能被使用時會將其保持在內(nèi)存中。

例如:

let john = { name: "John" };

// 該對象能被訪問,john 是它的引用

// 覆蓋引用
john = null;

// 該對象將會被從內(nèi)存中清除

通常,當對象、數(shù)組之類的數(shù)據(jù)結構在內(nèi)存中時,它們的子元素,如對象的屬性、數(shù)組的元素都被認為是可達的。

例如,如果把一個對象放入到數(shù)組中,那么只要這個數(shù)組存在,那么這個對象也就存在,即使沒有其他對該對象的引用。

就像這樣:

let john = { name: "John" };

let array = [ john ];

john = null; // 覆蓋引用

// 前面由 john 所引用的那個對象被存儲在了 array 中
// 所以它不會被垃圾回收機制回收
// 我們可以通過 array[0] 獲取到它

類似的,如果我們使用對象作為常規(guī) Map 的鍵,那么當 Map 存在時,該對象也將存在。它會占用內(nèi)存,并且不會被(垃圾回收機制)回收。

例如:

let john = { name: "John" };

let map = new Map();
map.set(john, "...");

john = null; // 覆蓋引用

// john 被存儲在了 map 中,
// 我們可以使用 map.keys() 來獲取它

WeakMap 在這方面有著根本上的不同。它不會阻止垃圾回收機制對作為鍵的對象(key object)的回收。

讓我們通過例子來看看這指的到底是什么。

WeakMap

WeakMap 和 Map 的第一個不同點就是,WeakMap 的鍵必須是對象,不能是原始值:

let weakMap = new WeakMap();

let obj = {};

weakMap.set(obj, "ok"); // 正常工作(以對象作為鍵)

// 不能使用字符串作為鍵
weakMap.set("test", "Whoops"); // Error,因為 "test" 不是一個對象

現(xiàn)在,如果我們在 weakMap 中使用一個對象作為鍵,并且沒有其他對這個對象的引用 —— 該對象將會被從內(nèi)存(和map)中自動清除。

let john = { name: "John" };

let weakMap = new WeakMap();
weakMap.set(john, "...");

john = null; // 覆蓋引用

// john 被從內(nèi)存中刪除了!

與上面常規(guī)的 Map 的例子相比,現(xiàn)在如果 john 僅僅是作為 WeakMap 的鍵而存在 —— 它將會被從 map(和內(nèi)存)中自動刪除。

WeakMap 不支持迭代以及 keys(),values() 和 entries() 方法。所以沒有辦法獲取 WeakMap 的所有鍵或值。

WeakMap 只有以下的方法:

  • ?weakMap.get(key)?
  • ?weakMap.set(key, value)?
  • ?weakMap.delete(key)?
  • ?weakMap.has(key)?

為什么會有這種限制呢?這是技術的原因。如果一個對象丟失了其它所有引用(就像上面示例中的 john),那么它就會被垃圾回收機制自動回收。但是在從技術的角度并不能準確知道 何時會被回收。

這些都是由 JavaScript 引擎決定的。JavaScript 引擎可能會選擇立即執(zhí)行內(nèi)存清理,如果現(xiàn)在正在發(fā)生很多刪除操作,那么 JavaScript 引擎可能就會選擇等一等,稍后再進行內(nèi)存清理。因此,從技術上講,WeakMap 的當前元素的數(shù)量是未知的。JavaScript 引擎可能清理了其中的垃圾,可能沒清理,也可能清理了一部分。因此,暫不支持訪問 WeakMap 的所有鍵/值的方法。

那么,在哪里我們會需要這樣的數(shù)據(jù)結構呢?

使用案例:額外的數(shù)據(jù)

WeakMap 的主要應用場景是 額外數(shù)據(jù)的存儲

假如我們正在處理一個“屬于”另一個代碼的一個對象,也可能是第三方庫,并想存儲一些與之相關的數(shù)據(jù),那么這些數(shù)據(jù)就應該與這個對象共存亡 —— 這時候 WeakMap 正是我們所需要的利器。

我們將這些數(shù)據(jù)放到 WeakMap 中,并使用該對象作為這些數(shù)據(jù)的鍵,那么當該對象被垃圾回收機制回收后,這些數(shù)據(jù)也會被自動清除。

weakMap.set(john, "secret documents");
// 如果 john 消失,secret documents 將會被自動清除

讓我們來看一個例子。

例如,我們有用于處理用戶訪問計數(shù)的代碼。收集到的信息被存儲在 map 中:一個用戶對象作為鍵,其訪問次數(shù)為值。當一個用戶離開時(該用戶對象將被垃圾回收機制回收),這時我們就不再需要他的訪問次數(shù)了。

下面是一個使用 Map 的計數(shù)函數(shù)的例子:

//  visitsCount.js
let visitsCountMap = new Map(); // map: user => visits count

// 遞增用戶來訪次數(shù)
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}

下面是其他部分的代碼,可能是使用它的其它代碼:

//  main.js
let john = { name: "John" };

countUser(john); // count his visits

// 不久之后,john 離開了
john = null;

現(xiàn)在,john 這個對象應該被垃圾回收,但它仍在內(nèi)存中,因為它是 visitsCountMap 中的一個鍵。

當我們移除用戶時,我們需要清理 visitsCountMap,否則它將在內(nèi)存中無限增大。在復雜的架構中,這種清理會成為一項繁重的任務。

我們可以通過使用 WeakMap 來避免這樣的問題:

//  visitsCount.js
let visitsCountMap = new WeakMap(); // weakmap: user => visits count

// 遞增用戶來訪次數(shù)
function countUser(user) {
  let count = visitsCountMap.get(user) || 0;
  visitsCountMap.set(user, count + 1);
}

現(xiàn)在我們不需要去清理 visitsCountMap 了。當 john 對象變成不可達時,即便它是 WeakMap 里的一個鍵,它也會連同它作為 WeakMap 里的鍵所對應的信息一同被從內(nèi)存中刪除。

使用案例:緩存

另外一個常見的例子是緩存。我們可以存儲(“緩存”)函數(shù)的結果,以便將來對同一個對象的調(diào)用可以重用這個結果。

為了實現(xiàn)這一點,我們可以使用 ?Map?(非最佳方案):

//  cache.js
let cache = new Map();

// 計算并記住結果
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* calculations of the result for */ obj;

    cache.set(obj, result);
  }

  return cache.get(obj);
}

// 現(xiàn)在我們在其它文件中使用 process()

//  main.js
let obj = {/* 假設我們有個對象 */};

let result1 = process(obj); // 計算完成

// ……稍后,來自代碼的另外一個地方……
let result2 = process(obj); // 取自緩存的被記憶的結果

// ……稍后,我們不再需要這個對象時:
obj = null;

alert(cache.size); // 1(?。≡搶ο笠廊辉?cache 中,并占據(jù)著內(nèi)存!)

對于多次調(diào)用同一個對象,它只需在第一次調(diào)用時計算出結果,之后的調(diào)用可以直接從 cache 中獲取。這樣做的缺點是,當我們不再需要這個對象的時候需要清理 cache。

如果我們用 WeakMap 替代 Map,便不會存在這個問題。當對象被垃圾回收時,對應緩存的結果也會被自動從內(nèi)存中清除。

//  cache.js
let cache = new WeakMap();

// 計算并記結果
function process(obj) {
  if (!cache.has(obj)) {
    let result = /* calculate the result for */ obj;

    cache.set(obj, result);
  }

  return cache.get(obj);
}

//  main.js
let obj = {/* some object */};

let result1 = process(obj);
let result2 = process(obj);

// ……稍后,我們不再需要這個對象時:
obj = null;

// 無法獲取 cache.size,因為它是一個 WeakMap,
// 要么是 0,或即將變?yōu)?0
// 當 obj 被垃圾回收,緩存的數(shù)據(jù)也會被清除

WeakSet

WeakSet 的表現(xiàn)類似:

  • 與 ?Set? 類似,但是我們只能向 ?WeakSet? 添加對象(而不能是原始值)。
  • 對象只有在其它某個(些)地方能被訪問的時候,才能留在 ?WeakSet? 中。
  • 跟 ?Set? 一樣,?WeakSet? 支持 ?add?,?has? 和 ?delete? 方法,但不支持 ?size? 和 ?keys()?,并且不可迭代。

變“弱(weak)”的同時,它也可以作為額外的存儲空間。但并非針對任意數(shù)據(jù),而是針對“是/否”的事實。WeakSet 的元素可能代表著有關該對象的某些信息。

例如,我們可以將用戶添加到 WeakSet 中,以追蹤訪問過我們網(wǎng)站的用戶:

let visitedSet = new WeakSet();

let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };

visitedSet.add(john); // John 訪問了我們
visitedSet.add(pete); // 然后是 Pete
visitedSet.add(john); // John 再次訪問

// visitedSet 現(xiàn)在有兩個用戶了

// 檢查 John 是否來訪過?
alert(visitedSet.has(john)); // true

// 檢查 Mary 是否來訪過?
alert(visitedSet.has(mary)); // false

john = null;

// visitedSet 將被自動清理(即自動清除其中已失效的值 john)

WeakMap 和 WeakSet 最明顯的局限性就是不能迭代,并且無法獲取所有當前內(nèi)容。那樣可能會造成不便,但是并不會阻止 WeakMap/WeakSet 完成其主要工作 —— 為在其它地方存儲/管理的對象數(shù)據(jù)提供“額外”存儲。

總結

WeakMap 是類似于 Map 的集合,它僅允許對象作為鍵,并且一旦通過其他方式無法訪問這些對象,垃圾回收便會將這些對象與其關聯(lián)值一同刪除。

WeakSet 是類似于 Set 的集合,它僅存儲對象,并且一旦通過其他方式無法訪問這些對象,垃圾回收便會將這些對象刪除。

它們的主要優(yōu)點是它們對對象是弱引用,所以被它們引用的對象很容易地被垃圾收集器移除。

這是以不支持 clearsize、keys、values 等作為代價換來的……

WeakMap 和 WeakSet 被用作“主要”對象存儲之外的“輔助”數(shù)據(jù)結構。一旦將對象從主存儲器中刪除,如果該對象僅被用作 WeakMap 或 WeakSet 的鍵,那么該對象將被自動清除。

任務


存儲 "unread" 標識

重要程度: 5

這里有一個 messages 數(shù)組:

let messages = [
  {text: "Hello", from: "John"},
  {text: "How goes?", from: "John"},
  {text: "See you soon", from: "Alice"}
];

你的代碼可以訪問它,但是 message 是由其他人的代碼管理的。該代碼會定期添加新消息,刪除舊消息,但是你不知道這些操作確切的發(fā)生時間。

現(xiàn)在,你應該使用什么數(shù)據(jù)結構來保存關于消息“是否已讀”的信息?該結構必須很適合對給定的 message 對象給出“它讀了嗎?”的答案。

P.S. 當一個消息被從 ?messages? 中刪除后,它應該也從你的數(shù)據(jù)結構中消失。

P.S. 我們不能修改 message 對象,例如向其添加我們的屬性。因為它們是由其他人的代碼管理的,我們修改該數(shù)據(jù)可能會導致不好的后果。


解決方案

讓我們將已讀消息存儲在 WeakSet 中:

let messages = [
  {text: "Hello", from: "John"},
  {text: "How goes?", from: "John"},
  {text: "See you soon", from: "Alice"}
];

let readMessages = new WeakSet();

// 兩個消息已讀
readMessages.add(messages[0]);
readMessages.add(messages[1]);
// readMessages 包含兩個元素

// ……讓我們再讀一遍第一條消息!
readMessages.add(messages[0]);
// readMessages 仍然有兩個不重復的元素

// 回答:message[0] 已讀?
alert("Read message 0: " + readMessages.has(messages[0])); // true

messages.shift();
// 現(xiàn)在 readMessages 有一個元素(技術上來講,內(nèi)存可能稍后才會被清理)

WeakSet 允許存儲一系列的消息,并且很容易就能檢查它是否包含某個消息。

它會自動清理自身。代價是,我們不能對它進行迭代,也不能直接從中獲取“所有已讀消息”。但是,我們可以通過遍歷所有消息,然后找出存在于 set 的那些消息來完成這個功能。

另一種不同的解決方案可以是,在讀取消息后向消息添加諸如 message.isRead=true 之類的屬性。由于 messages 對象是由另一個代碼管理的,因此通常不建議這樣做,但是我們可以使用 symbol 屬性來避免沖突。

像這樣:

// symbol 屬性僅對于我們的代碼是已知的
let isRead = Symbol("isRead");
messages[0][isRead] = true;

現(xiàn)在,第三方代碼可能看不到我們的額外屬性。

盡管 symbol 可以降低出現(xiàn)問題的可能性,但從架構的角度來看,還是使用 ?WeakSet? 更好。


保存閱讀日期

重要程度: 5

這兒有一個和 上一個任務 類似的 messages 數(shù)組。場景也相似。

let messages = [
  {text: "Hello", from: "John"},
  {text: "How goes?", from: "John"},
  {text: "See you soon", from: "Alice"}
];

現(xiàn)在的問題是:你建議采用什么數(shù)據(jù)結構來保存信息:“消息是什么時候被閱讀的?”。

在前一個任務中我們只需要保存“是/否”?,F(xiàn)在我們需要保存日期,并且它應該在消息被垃圾回收時也被從內(nèi)存中清除。

P.S. 日期可以存儲為內(nèi)建的 Date 類的對象,稍后我們將進行介紹。


解決方案

我們可以使用 WeakMap 保存日期:

let messages = [
  {text: "Hello", from: "John"},
  {text: "How goes?", from: "John"},
  {text: "See you soon", from: "Alice"}
];

let readMap = new WeakMap();

readMap.set(messages[0], new Date(2017, 1, 1));
// 我們稍后將學習 Date 對象


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號