W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗值獎勵
我們從前面的 垃圾回收 章節(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
和 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ù)結構呢?
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
的表現(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)點是它們對對象是弱引用,所以被它們引用的對象很容易地被垃圾收集器移除。
這是以不支持 clear
、size
、keys
、values
等作為代價換來的……
WeakMap
和 WeakSet
被用作“主要”對象存儲之外的“輔助”數(shù)據(jù)結構。一旦將對象從主存儲器中刪除,如果該對象僅被用作 WeakMap
或 WeakSet
的鍵,那么該對象將被自動清除。
重要程度: 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 對象
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: