從 垃圾回收 章節中我們知道,JavaScript 引擎會在值「可觸及」且有機會被使用的期間將其保留在記憶體中。
例如
let john = { name: "John" };
// the object can be accessed, john is the reference to it
// overwrite the reference
john = null;
// the object will be removed from memory
通常,物件的屬性或陣列或其他資料結構的元素會被視為可觸及,並在該資料結構存在於記憶體中時保留在記憶體中。
例如,如果我們將一個物件放入陣列中,那麼在陣列存在的期間,物件也會存在,即使沒有其他對它的參照。
像這樣
let john = { name: "John" };
let array = [ john ];
john = null; // overwrite the reference
// the object previously referenced by john is stored inside the array
// therefore it won't be garbage-collected
// we can get it as array[0]
類似地,如果我們在一般 Map
中使用物件作為鍵,則在 Map
存在的期間,該物件也會存在。它會佔用記憶體,而且可能不會被垃圾回收。
例如
let john = { name: "John" };
let map = new Map();
map.set(john, "...");
john = null; // overwrite the reference
// john is stored inside the map,
// we can get it by using map.keys()
WeakMap
在這方面有根本上的不同。它不會阻止鍵物件被垃圾回收。
讓我們透過範例來看看它的意思。
WeakMap
Map
和 WeakMap
之間的第一個差異在於鍵必須是物件,而不是基本值
let weakMap = new WeakMap();
let obj = {};
weakMap.set(obj, "ok"); // works fine (object key)
// can't use a string as the key
weakMap.set("test", "Whoops"); // Error, because "test" is not an object
現在,如果我們在其中使用物件作為鍵,而且沒有其他對該物件的參照,它將自動從記憶體(和映射)中移除。
let john = { name: "John" };
let weakMap = new WeakMap();
weakMap.set(john, "...");
john = null; // overwrite the reference
// john is removed from memory!
將它與上述一般 Map
範例進行比較。現在,如果 john
僅存在於 WeakMap
的鍵中,它將自動從映射(和記憶體)中刪除。
WeakMap
不支援迭代和方法 keys()
、values()
、entries()
,因此無法從中取得所有鍵或值。
WeakMap
僅有以下方法
為什麼會有這樣的限制?這是基於技術原因。如果一個物件已失去所有其他參照(例如上述程式碼中的 john
),則它將自動被垃圾回收。但技術上並未明確指定「何時會執行清除」。
這是由 JavaScript 引擎決定的。它可能會選擇立即執行記憶體清除,或等到稍後有更多刪除時再執行清除。因此,技術上來說,WeakMap
的目前元素數量是未知的。引擎可能已清除它,或尚未清除,或只部分清除。因此,不支援存取所有鍵/值的函式。
現在,我們在哪裡需要這樣的資料結構?
使用案例:附加資料
WeakMap
的主要應用領域是附加資料儲存。
如果我們使用「屬於」其他程式碼的物件,甚至可能是第三方函式庫,而且想要儲存與其相關的資料,這些資料應該只在物件存在時存在,那麼 WeakMap
正是需要的。
我們將資料放入 WeakMap
,使用物件作為金鑰,當物件被垃圾回收時,該資料也會自動消失。
weakMap.set(john, "secret documents");
// if john dies, secret documents will be destroyed automatically
我們來看一個範例。
例如,我們有程式碼來記錄使用者的造訪次數。這些資訊儲存在一個對應中:使用者物件是金鑰,造訪次數是值。當使用者離開時(其物件被垃圾回收),我們不想要再儲存其造訪次數。
以下是使用 Map
的計數函數範例
// 📁 visitsCount.js
let visitsCountMap = new Map(); // map: user => visits count
// increase the 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
// later john leaves us
john = null;
現在,john
物件應該被垃圾回收,但仍留在記憶體中,因為它是 visitsCountMap
中的金鑰。
當我們移除使用者時,需要清除 visitsCountMap
,否則它會在記憶體中無限增長。在複雜的架構中,這種清除可能會變成一項繁瑣的任務。
我們可以改用 WeakMap
來避免這種情況
// 📁 visitsCount.js
let visitsCountMap = new WeakMap(); // weakmap: user => visits count
// increase the 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();
// calculate and remember the result
function process(obj) {
if (!cache.has(obj)) {
let result = /* calculations of the result for */ obj;
cache.set(obj, result);
return result;
}
return cache.get(obj);
}
// Now we use process() in another file:
// 📁 main.js
let obj = {/* let's say we have an object */};
let result1 = process(obj); // calculated
// ...later, from another place of the code...
let result2 = process(obj); // remembered result taken from cache
// ...later, when the object is not needed any more:
obj = null;
alert(cache.size); // 1 (Ouch! The object is still in cache, taking memory!)
對於使用同一個物件的 process(obj)
多次呼叫,它只在第一次計算結果,然後從 cache
中取得。缺點是當不再需要物件時,我們需要清除 cache
。
如果我們將 Map
替換為 WeakMap
,那麼這個問題就會消失。快取結果會在物件被垃圾回收後自動從記憶體中移除。
// 📁 cache.js
let cache = new WeakMap();
// calculate and remember the result
function process(obj) {
if (!cache.has(obj)) {
let result = /* calculate the result for */ obj;
cache.set(obj, result);
return result;
}
return cache.get(obj);
}
// 📁 main.js
let obj = {/* some object */};
let result1 = process(obj);
let result2 = process(obj);
// ...later, when the object is not needed any more:
obj = null;
// Can't get cache.size, as it's a WeakMap,
// but it's 0 or soon be 0
// When obj gets garbage collected, cached data will be removed as well
WeakSet
WeakSet
的行為類似
- 它類似於
Set
,但我們只能將物件新增到WeakSet
(而不是原始值)。 - 當物件可以從其他地方存取時,它就會存在於集合中。
- 與
Set
一樣,它支援add
、has
和delete
,但不支援size
、keys()
和任何反覆運算。
由於是「弱」的,它也可用作額外的儲存空間。但不是用於任意資料,而是用於「是/否」的事實。在 WeakSet
中的成員資格可能表示物件的某些意義。
例如,我們可以將使用者新增到 WeakSet
,以追蹤造訪我們網站的使用者
let visitedSet = new WeakSet();
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
visitedSet.add(john); // John visited us
visitedSet.add(pete); // Then Pete
visitedSet.add(john); // John again
// visitedSet has 2 users now
// check if John visited?
alert(visitedSet.has(john)); // true
// check if Mary visited?
alert(visitedSet.has(mary)); // false
john = null;
// visitedSet will be cleaned automatically
WeakMap
和 WeakSet
最顯著的限制是沒有反覆運算,而且無法取得所有目前的內容。這可能看起來不方便,但並不會阻止 WeakMap/WeakSet
執行它們的主要工作,也就是為儲存在其他地方的物件提供「額外的」資料儲存空間。
摘要
WeakMap
是類似 Map
的集合,它只允許物件作為鍵,而且當物件無法透過其他方式存取時,就會將它們連同關聯的值一起移除。
WeakSet
是類似 Set
的集合,它只儲存物件,而且當物件無法透過其他方式存取時,就會將它們移除。
它們的主要優點是它們對物件具有弱參考,因此垃圾收集器可以輕鬆移除它們。
這會導致無法支援 clear
、size
、keys
、values
等功能。
WeakMap
和 WeakSet
用作「次要」資料結構,以補充「主要」物件儲存空間。一旦物件從主要儲存空間中移除,如果它只存在於 WeakMap
的鍵中或 WeakSet
中,它就會自動清除。
留言
<code>
標籤,若要插入多行程式碼,請將其包覆在<pre>
標籤中,若要插入 10 行以上的程式碼,請使用沙盒 (plnkr、jsbin、codepen…)