我們從前面的 垃圾回收 章節中知道,JavaScript 引擎在值“可達”和可能被使用時會將其保持在內存中。
例如:
let john = { name: "John" }; // 該對象能被訪問,john 是它的引用 // 覆蓋引用 john = null; // 該對象將會被從內存中清除
通常,當對象、數組之類的數據結構在內存中時,它們的子元素,如對象的屬性、數組的元素都被認爲是可達的。
例如,如果把壹個對象放入到數組中,那麽只要這個數組存在,那麽這個對象也就存在,即使沒有其他對該對象的引用。
就像這樣:
let john = { name: "John" }; let array = [ john ]; john = null; // 覆蓋引用 // 前面由 john 所引用的那個對象被存儲在了 array 中 // 所以它不會被垃圾回收機制回收 // 我們可以通過 array[0] 獲取到它
類似的,如果我們使用對象作爲常規 Map
的鍵,那麽當 Map
存在時,該對象也將存在。它會占用內存,並且不會被(垃圾回收機制)回收。
例如:
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" 不是壹個對象
現在,如果我們在 weakMap 中使用壹個對象作爲鍵,並且沒有其他對這個對象的引用 —— 該對象將會被從內存(和map)中自動清除。
let john = { name: "John" }; let weakMap = new WeakMap(); weakMap.set(john, "..."); john = null; // 覆蓋引用 // john 被從內存中刪除了!
與上面常規的 Map
的例子相比,現在如果 john
僅僅是作爲 WeakMap
的鍵而存在 —— 它將會被從 map(和內存)中自動刪除。
WeakMap
不支持叠代以及 keys()
,values()
和 entries()
方法。所以沒有辦法獲取 WeakMap
的所有鍵或值。
WeakMap
只有以下的方法:
weakMap.get(key)
weakMap.set(key, value)
weakMap.delete(key)
weakMap.has(key)
爲什麽會有這種限制呢?這是技術的原因。如果壹個對象丟失了其它所有引用(就像上面示例中的 john
),那麽它就會被垃圾回收機制自動回收。但是在從技術的角度並不能准確知道 何時會被回收。
這些都是由 JavaScript 引擎決定的。JavaScript 引擎可能會選擇立即執行內存清理,如果現在正在發生很多刪除操作,那麽 JavaScript 引擎可能就會選擇等壹等,稍後再進行內存清理。因此,從技術上講,WeakMap
的當前元素的數量是未知的。JavaScript 引擎可能清理了其中的垃圾,可能沒清理,也可能清理了壹部分。因此,暫不支持訪問 WeakMap
的所有鍵/值的方法。
那麽,在哪裏我們會需要這樣的數據結構呢?
WeakMap
的主要應用場景是 額外數據的存儲。
假如我們正在處理壹個“屬于”另壹個代碼的壹個對象,也可能是第三方庫,並想存儲壹些與之相關的數據,那麽這些數據就應該與這個對象共存亡 —— 這時候 WeakMap
正是我們所需要的利器。
我們將這些數據放到 WeakMap
中,並使用該對象作爲這些數據的鍵,那麽當該對象被垃圾回收機制回收後,這些數據也會被自動清除。
weakMap.set(john, "secret documents"); // 如果 john 消失,secret documents 將會被自動清除
讓我們來看壹個例子。
例如,我們有用于處理用戶訪問計數的代碼。收集到的信息被存儲在 map 中:壹個用戶對象作爲鍵,其訪問次數爲值。當壹個用戶離開時(該用戶對象將被垃圾回收機制回收),這時我們就不再需要他的訪問次數了。
下面是壹個使用 Map
的計數函數的例子:
// ? visitsCount.js let visitsCountMap = new Map(); // map: user => visits count // 遞增用戶來訪次數 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;
現在,john
這個對象應該被垃圾回收,但它仍在內存中,因爲它是 visitsCountMap
中的壹個鍵。
當我們移除用戶時,我們需要清理 visitsCountMap
,否則它將在內存中無限增大。在複雜的架構中,這種清理會成爲壹項繁重的任務。
我們可以通過使用 WeakMap
來避免這樣的問題:
// ? visitsCount.js let visitsCountMap = new WeakMap(); // weakmap: user => visits count // 遞增用戶來訪次數 function countUser(user) { let count = visitsCountMap.get(user) || 0; visitsCountMap.set(user, count + 1); }
現在我們不需要去清理 visitsCountMap
了。當 john
對象變成不可達時,即便它是 WeakMap
裏的壹個鍵,它也會連同它作爲 WeakMap
裏的鍵所對應的信息壹同被從內存中刪除。
另外壹個常見的例子是緩存。我們可以存儲(“緩存”)函數的結果,以便將來對同壹個對象的調用可以重用這個結果。
爲了實現這壹點,我們可以使用 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); } // 現在我們在其它文件中使用 process() // ? main.js let obj = {/* 假設我們有個對象 */}; let result1 = process(obj); // 計算完成 // ……稍後,來自代碼的另外壹個地方…… let result2 = process(obj); // 取自緩存的被記憶的結果 // ……稍後,我們不再需要這個對象時: obj = null; alert(cache.size); // 1(啊!該對象依然在 cache 中,並占據著內存!)
對于多次調用同壹個對象,它只需在第壹次調用時計算出結果,之後的調用可以直接從 cache
中獲取。這樣做的缺點是,當我們不再需要這個對象的時候需要清理 cache
。
如果我們用 WeakMap
替代 Map
,便不會存在這個問題。當對象被垃圾回收時,對應緩存的結果也會被自動從內存中清除。
// ? 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,或即將變爲 0 // 當 obj 被垃圾回收,緩存的數據也會被清除
WeakSet
的表現類似:
與 Set
類似,但是我們只能向 WeakSet
添加對象(而不能是原始值)。
對象只有在其它某個(些)地方能被訪問的時候,才能留在 WeakSet
中。
跟 Set
壹樣,WeakSet
支持 add
,has
和 delete
方法,但不支持 size
和 keys()
,並且不可叠代。
變“弱(weak)”的同時,它也可以作爲額外的存儲空間。但並非針對任意數據,而是針對“是/否”的事實。WeakSet
的元素可能代表著有關該對象的某些信息。
例如,我們可以將用戶添加到 WeakSet
中,以追蹤訪問過我們網站的用戶:
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 現在有兩個用戶了 // 檢查 John 是否來訪過? alert(visitedSet.has(john)); // true // 檢查 Mary 是否來訪過? alert(visitedSet.has(mary)); // false john = null; // visitedSet 將被自動清理(即自動清除其中已失效的值 john)
WeakMap
和 WeakSet
最明顯的局限性就是不能叠代,並且無法獲取所有當前內容。那樣可能會造成不便,但是並不會阻止 WeakMap/WeakSet
完成其主要工作 —— 爲在其它地方存儲/管理的對象數據提供“額外”存儲。
WeakMap
是類似于 Map
的集合,它僅允許對象作爲鍵,並且壹旦通過其他方式無法訪問這些對象,垃圾回收便會將這些對象與其關聯值壹同刪除。
WeakSet
是類似于 Set
的集合,它僅存儲對象,並且壹旦通過其他方式無法訪問這些對象,垃圾回收便會將這些對象刪除。
它們的主要優點是它們對對象是弱引用,所以被它們引用的對象很容易地被垃圾收集器移除。
這是以不支持 clear
、size
、keys
、values
等作爲代價換來的……
WeakMap
和 WeakSet
被用作“主要”對象存儲之外的“輔助”數據結構。壹旦將對象從主存儲器中刪除,如果該對象僅被用作 WeakMap
或 WeakSet
的鍵,那麽該對象將被自動清除。
重要程度: 5
這裏有壹個 messages 數組:
let messages = [ {text: "Hello", from: "John"}, {text: "How goes?", from: "John"}, {text: "See you soon", from: "Alice"} ];
妳的代碼可以訪問它,但是 message 是由其他人的代碼管理的。該代碼會定期添加新消息,刪除舊消息,但是妳不知道這些操作確切的發生時間。
現在,妳應該使用什麽數據結構來保存關于消息“是否已讀”的信息?該結構必須很適合對給定的 message 對象給出“它讀了嗎?”的答案。
P.S. 當壹個消息被從 messages
中刪除後,它應該也從妳的數據結構中消失。
P.S. 我們不能修改 message 對象,例如向其添加我們的屬性。因爲它們是由其他人的代碼管理的,我們修改該數據可能會導致不好的後果。
讓我們將已讀消息存儲在 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(); // 現在 readMessages 有壹個元素(技術上來講,內存可能稍後才會被清理)
WeakSet
允許存儲壹系列的消息,並且很容易就能檢查它是否包含某個消息。
它會自動清理自身。代價是,我們不能對它進行叠代,也不能直接從中獲取“所有已讀消息”。但是,我們可以通過遍曆所有消息,然後找出存在于 set 的那些消息來完成這個功能。
另壹種不同的解決方案可以是,在讀取消息後向消息添加諸如 message.isRead=true
之類的屬性。由于 messages
對象是由另壹個代碼管理的,因此通常不建議這樣做,但是我們可以使用 symbol 屬性來避免沖突。
像這樣:
// symbol 屬性僅對于我們的代碼是已知的 let isRead = Symbol("isRead"); messages[0][isRead] = true;
現在,第三方代碼可能看不到我們的額外屬性。
盡管 symbol 可以降低出現問題的可能性,但從架構的角度來看,還是使用 WeakSet
更好。
重要程度: 5
這兒有壹個和 上壹個任務 類似的 messages
數組。場景也相似。
let messages = [ {text: "Hello", from: "John"}, {text: "How goes?", from: "John"}, {text: "See you soon", from: "Alice"} ];
現在的問題是:妳建議采用什麽數據結構來保存信息:“消息是什麽時候被閱讀的?”。
在前壹個任務中我們只需要保存“是/否”。現在我們需要保存日期,並且它應該在消息被垃圾回收時也被從內存中清除。
P.S. 日期可以存儲爲內建的 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 對象