W3Cschool
恭喜您成為首批注冊(cè)用戶
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
我們從前面的 垃圾回收 章節(jié)中知道,JavaScript 引擎在值“可達(dá)”和可能被使用時(shí)會(huì)將其保持在內(nèi)存中。
例如:
let john = { name: "John" };
// 該對(duì)象能被訪問(wèn),john 是它的引用
// 覆蓋引用
john = null;
// 該對(duì)象將會(huì)被從內(nèi)存中清除
通常,當(dāng)對(duì)象、數(shù)組之類(lèi)的數(shù)據(jù)結(jié)構(gòu)在內(nèi)存中時(shí),它們的子元素,如對(duì)象的屬性、數(shù)組的元素都被認(rèn)為是可達(dá)的。
例如,如果把一個(gè)對(duì)象放入到數(shù)組中,那么只要這個(gè)數(shù)組存在,那么這個(gè)對(duì)象也就存在,即使沒(méi)有其他對(duì)該對(duì)象的引用。
就像這樣:
let john = { name: "John" };
let array = [ john ];
john = null; // 覆蓋引用
// 前面由 john 所引用的那個(gè)對(duì)象被存儲(chǔ)在了 array 中
// 所以它不會(huì)被垃圾回收機(jī)制回收
// 我們可以通過(guò) array[0] 獲取到它
類(lèi)似的,如果我們使用對(duì)象作為常規(guī) Map
的鍵,那么當(dāng) Map
存在時(shí),該對(duì)象也將存在。它會(huì)占用內(nèi)存,并且不會(huì)被(垃圾回收機(jī)制)回收。
例如:
let john = { name: "John" };
let map = new Map();
map.set(john, "...");
john = null; // 覆蓋引用
// john 被存儲(chǔ)在了 map 中,
// 我們可以使用 map.keys() 來(lái)獲取它
WeakMap
在這方面有著根本上的不同。它不會(huì)阻止垃圾回收機(jī)制對(duì)作為鍵的對(duì)象(key object)的回收。
讓我們通過(guò)例子來(lái)看看這指的到底是什么。
WeakMap
和 Map
的第一個(gè)不同點(diǎn)就是,WeakMap
的鍵必須是對(duì)象,不能是原始值:
let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, "ok"); // 正常工作(以對(duì)象作為鍵)
// 不能使用字符串作為鍵
weakMap.set("test", "Whoops"); // Error,因?yàn)?"test" 不是一個(gè)對(duì)象
現(xiàn)在,如果我們?cè)?weakMap 中使用一個(gè)對(duì)象作為鍵,并且沒(méi)有其他對(duì)這個(gè)對(duì)象的引用 —— 該對(duì)象將會(huì)被從內(nèi)存(和map)中自動(dòng)清除。
let john = { name: "John" };
let weakMap = new WeakMap();
weakMap.set(john, "...");
john = null; // 覆蓋引用
// john 被從內(nèi)存中刪除了!
與上面常規(guī)的 Map
的例子相比,現(xiàn)在如果 john
僅僅是作為 WeakMap
的鍵而存在 —— 它將會(huì)被從 map(和內(nèi)存)中自動(dòng)刪除。
WeakMap
不支持迭代以及 keys()
,values()
和 entries()
方法。所以沒(méi)有辦法獲取 WeakMap
的所有鍵或值。
WeakMap
只有以下的方法:
weakMap.get(key)
?weakMap.set(key, value)
?weakMap.delete(key)
?weakMap.has(key)
?為什么會(huì)有這種限制呢?這是技術(shù)的原因。如果一個(gè)對(duì)象丟失了其它所有引用(就像上面示例中的 john
),那么它就會(huì)被垃圾回收機(jī)制自動(dòng)回收。但是在從技術(shù)的角度并不能準(zhǔn)確知道 何時(shí)會(huì)被回收。
這些都是由 JavaScript 引擎決定的。JavaScript 引擎可能會(huì)選擇立即執(zhí)行內(nèi)存清理,如果現(xiàn)在正在發(fā)生很多刪除操作,那么 JavaScript 引擎可能就會(huì)選擇等一等,稍后再進(jìn)行內(nèi)存清理。因此,從技術(shù)上講,WeakMap
的當(dāng)前元素的數(shù)量是未知的。JavaScript 引擎可能清理了其中的垃圾,可能沒(méi)清理,也可能清理了一部分。因此,暫不支持訪問(wèn) WeakMap
的所有鍵/值的方法。
那么,在哪里我們會(huì)需要這樣的數(shù)據(jù)結(jié)構(gòu)呢?
WeakMap
的主要應(yīng)用場(chǎng)景是 額外數(shù)據(jù)的存儲(chǔ)。
假如我們正在處理一個(gè)“屬于”另一個(gè)代碼的一個(gè)對(duì)象,也可能是第三方庫(kù),并想存儲(chǔ)一些與之相關(guān)的數(shù)據(jù),那么這些數(shù)據(jù)就應(yīng)該與這個(gè)對(duì)象共存亡 —— 這時(shí)候 WeakMap
正是我們所需要的利器。
我們將這些數(shù)據(jù)放到 WeakMap
中,并使用該對(duì)象作為這些數(shù)據(jù)的鍵,那么當(dāng)該對(duì)象被垃圾回收機(jī)制回收后,這些數(shù)據(jù)也會(huì)被自動(dòng)清除。
weakMap.set(john, "secret documents");
// 如果 john 消失,secret documents 將會(huì)被自動(dòng)清除
讓我們來(lái)看一個(gè)例子。
例如,我們有用于處理用戶訪問(wèn)計(jì)數(shù)的代碼。收集到的信息被存儲(chǔ)在 map 中:一個(gè)用戶對(duì)象作為鍵,其訪問(wèn)次數(shù)為值。當(dāng)一個(gè)用戶離開(kāi)時(shí)(該用戶對(duì)象將被垃圾回收機(jī)制回收),這時(shí)我們就不再需要他的訪問(wèn)次數(shù)了。
下面是一個(gè)使用 Map
的計(jì)數(shù)函數(shù)的例子:
// visitsCount.js
let visitsCountMap = new Map(); // map: user => visits count
// 遞增用戶來(lái)訪次數(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 離開(kāi)了
john = null;
現(xiàn)在,john
這個(gè)對(duì)象應(yīng)該被垃圾回收,但它仍在內(nèi)存中,因?yàn)樗?nbsp;visitsCountMap
中的一個(gè)鍵。
當(dāng)我們移除用戶時(shí),我們需要清理 visitsCountMap
,否則它將在內(nèi)存中無(wú)限增大。在復(fù)雜的架構(gòu)中,這種清理會(huì)成為一項(xiàng)繁重的任務(wù)。
我們可以通過(guò)使用 WeakMap
來(lái)避免這樣的問(wèn)題:
// visitsCount.js
let visitsCountMap = new WeakMap(); // weakmap: user => visits count
// 遞增用戶來(lái)訪次數(shù)
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
現(xiàn)在我們不需要去清理 visitsCountMap
了。當(dāng) john
對(duì)象變成不可達(dá)時(shí),即便它是 WeakMap
里的一個(gè)鍵,它也會(huì)連同它作為 WeakMap
里的鍵所對(duì)應(yīng)的信息一同被從內(nèi)存中刪除。
另外一個(gè)常見(jiàn)的例子是緩存。我們可以存儲(chǔ)(“緩存”)函數(shù)的結(jié)果,以便將來(lái)對(duì)同一個(gè)對(duì)象的調(diào)用可以重用這個(gè)結(jié)果。
為了實(shí)現(xiàn)這一點(diǎn),我們可以使用 ?Map
?(非最佳方案):
// cache.js
let cache = new Map();
// 計(jì)算并記住結(jié)果
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)在我們?cè)谄渌募惺褂?process()
// main.js
let obj = {/* 假設(shè)我們有個(gè)對(duì)象 */};
let result1 = process(obj); // 計(jì)算完成
// ……稍后,來(lái)自代碼的另外一個(gè)地方……
let result2 = process(obj); // 取自緩存的被記憶的結(jié)果
// ……稍后,我們不再需要這個(gè)對(duì)象時(shí):
obj = null;
alert(cache.size); // 1(啊!該對(duì)象依然在 cache 中,并占據(jù)著內(nèi)存?。?/code>
對(duì)于多次調(diào)用同一個(gè)對(duì)象,它只需在第一次調(diào)用時(shí)計(jì)算出結(jié)果,之后的調(diào)用可以直接從 cache
中獲取。這樣做的缺點(diǎn)是,當(dāng)我們不再需要這個(gè)對(duì)象的時(shí)候需要清理 cache
。
如果我們用 WeakMap
替代 Map
,便不會(huì)存在這個(gè)問(wèn)題。當(dāng)對(duì)象被垃圾回收時(shí),對(duì)應(yīng)緩存的結(jié)果也會(huì)被自動(dòng)從內(nèi)存中清除。
// cache.js
let cache = new WeakMap();
// 計(jì)算并記結(jié)果
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);
// ……稍后,我們不再需要這個(gè)對(duì)象時(shí):
obj = null;
// 無(wú)法獲取 cache.size,因?yàn)樗且粋€(gè) WeakMap,
// 要么是 0,或即將變?yōu)?0
// 當(dāng) obj 被垃圾回收,緩存的數(shù)據(jù)也會(huì)被清除
WeakSet
的表現(xiàn)類(lèi)似:
Set
? 類(lèi)似,但是我們只能向 ?WeakSet
? 添加對(duì)象(而不能是原始值)。WeakSet
? 中。Set
? 一樣,?WeakSet
? 支持 ?add
?,?has
? 和 ?delete
? 方法,但不支持 ?size
? 和 ?keys()
?,并且不可迭代。變“弱(weak)”的同時(shí),它也可以作為額外的存儲(chǔ)空間。但并非針對(duì)任意數(shù)據(jù),而是針對(duì)“是/否”的事實(shí)。WeakSet
的元素可能代表著有關(guān)該對(duì)象的某些信息。
例如,我們可以將用戶添加到 WeakSet
中,以追蹤訪問(wèn)過(guò)我們網(wǎng)站的用戶:
let visitedSet = new WeakSet();
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
visitedSet.add(john); // John 訪問(wèn)了我們
visitedSet.add(pete); // 然后是 Pete
visitedSet.add(john); // John 再次訪問(wèn)
// visitedSet 現(xiàn)在有兩個(gè)用戶了
// 檢查 John 是否來(lái)訪過(guò)?
alert(visitedSet.has(john)); // true
// 檢查 Mary 是否來(lái)訪過(guò)?
alert(visitedSet.has(mary)); // false
john = null;
// visitedSet 將被自動(dòng)清理(即自動(dòng)清除其中已失效的值 john)
WeakMap
和 WeakSet
最明顯的局限性就是不能迭代,并且無(wú)法獲取所有當(dāng)前內(nèi)容。那樣可能會(huì)造成不便,但是并不會(huì)阻止 WeakMap/WeakSet
完成其主要工作 —— 為在其它地方存儲(chǔ)/管理的對(duì)象數(shù)據(jù)提供“額外”存儲(chǔ)。
WeakMap
是類(lèi)似于 Map
的集合,它僅允許對(duì)象作為鍵,并且一旦通過(guò)其他方式無(wú)法訪問(wèn)這些對(duì)象,垃圾回收便會(huì)將這些對(duì)象與其關(guān)聯(lián)值一同刪除。
WeakSet
是類(lèi)似于 Set
的集合,它僅存儲(chǔ)對(duì)象,并且一旦通過(guò)其他方式無(wú)法訪問(wèn)這些對(duì)象,垃圾回收便會(huì)將這些對(duì)象刪除。
它們的主要優(yōu)點(diǎn)是它們對(duì)對(duì)象是弱引用,所以被它們引用的對(duì)象很容易地被垃圾收集器移除。
這是以不支持 clear
、size
、keys
、values
等作為代價(jià)換來(lái)的……
WeakMap
和 WeakSet
被用作“主要”對(duì)象存儲(chǔ)之外的“輔助”數(shù)據(jù)結(jié)構(gòu)。一旦將對(duì)象從主存儲(chǔ)器中刪除,如果該對(duì)象僅被用作 WeakMap
或 WeakSet
的鍵,那么該對(duì)象將被自動(dòng)清除。
重要程度: 5
這里有一個(gè) messages 數(shù)組:
let messages = [
{text: "Hello", from: "John"},
{text: "How goes?", from: "John"},
{text: "See you soon", from: "Alice"}
];
你的代碼可以訪問(wèn)它,但是 message 是由其他人的代碼管理的。該代碼會(huì)定期添加新消息,刪除舊消息,但是你不知道這些操作確切的發(fā)生時(shí)間。
現(xiàn)在,你應(yīng)該使用什么數(shù)據(jù)結(jié)構(gòu)來(lái)保存關(guān)于消息“是否已讀”的信息?該結(jié)構(gòu)必須很適合對(duì)給定的 message 對(duì)象給出“它讀了嗎?”的答案。
P.S. 當(dāng)一個(gè)消息被從 ?messages
? 中刪除后,它應(yīng)該也從你的數(shù)據(jù)結(jié)構(gòu)中消失。
P.S. 我們不能修改 message 對(duì)象,例如向其添加我們的屬性。因?yàn)樗鼈兪怯善渌说拇a管理的,我們修改該數(shù)據(jù)可能會(huì)導(dǎo)致不好的后果。
讓我們將已讀消息存儲(chǔ)在 WeakSet
中:
let messages = [
{text: "Hello", from: "John"},
{text: "How goes?", from: "John"},
{text: "See you soon", from: "Alice"}
];
let readMessages = new WeakSet();
// 兩個(gè)消息已讀
readMessages.add(messages[0]);
readMessages.add(messages[1]);
// readMessages 包含兩個(gè)元素
// ……讓我們?cè)僮x一遍第一條消息!
readMessages.add(messages[0]);
// readMessages 仍然有兩個(gè)不重復(fù)的元素
// 回答:message[0] 已讀?
alert("Read message 0: " + readMessages.has(messages[0])); // true
messages.shift();
// 現(xiàn)在 readMessages 有一個(gè)元素(技術(shù)上來(lái)講,內(nèi)存可能稍后才會(huì)被清理)
WeakSet
允許存儲(chǔ)一系列的消息,并且很容易就能檢查它是否包含某個(gè)消息。
它會(huì)自動(dòng)清理自身。代價(jià)是,我們不能對(duì)它進(jìn)行迭代,也不能直接從中獲取“所有已讀消息”。但是,我們可以通過(guò)遍歷所有消息,然后找出存在于 set 的那些消息來(lái)完成這個(gè)功能。
另一種不同的解決方案可以是,在讀取消息后向消息添加諸如 message.isRead=true
之類(lèi)的屬性。由于 messages
對(duì)象是由另一個(gè)代碼管理的,因此通常不建議這樣做,但是我們可以使用 symbol 屬性來(lái)避免沖突。
像這樣:
// symbol 屬性?xún)H對(duì)于我們的代碼是已知的
let isRead = Symbol("isRead");
messages[0][isRead] = true;
現(xiàn)在,第三方代碼可能看不到我們的額外屬性。
盡管 symbol 可以降低出現(xiàn)問(wèn)題的可能性,但從架構(gòu)的角度來(lái)看,還是使用 ?WeakSet
? 更好。
重要程度: 5
這兒有一個(gè)和 上一個(gè)任務(wù) 類(lèi)似的 messages
數(shù)組。場(chǎng)景也相似。
let messages = [
{text: "Hello", from: "John"},
{text: "How goes?", from: "John"},
{text: "See you soon", from: "Alice"}
];
現(xiàn)在的問(wèn)題是:你建議采用什么數(shù)據(jù)結(jié)構(gòu)來(lái)保存信息:“消息是什么時(shí)候被閱讀的?”。
在前一個(gè)任務(wù)中我們只需要保存“是/否”。現(xiàn)在我們需要保存日期,并且它應(yīng)該在消息被垃圾回收時(shí)也被從內(nèi)存中清除。
P.S. 日期可以存儲(chǔ)為內(nèi)建的 Date
類(lèi)的對(duì)象,稍后我們將進(jìn)行介紹。
我們可以使用 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));
// 我們稍后將學(xué)習(xí) Date 對(duì)象
Copyright©2021 w3cschool編程獅|閩ICP備15016281號(hào)-3|閩公網(wǎng)安備35020302033924號(hào)
違法和不良信息舉報(bào)電話:173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號(hào)
聯(lián)系方式:
更多建議: