WeakMap
Baseline 廣泛可用 *
WeakMap 是一種鍵值對集合,其鍵必須是物件或非註冊 Symbol,值為任意 JavaScript 型別,並且它不會對其鍵建立強引用。也就是說,一個物件作為 WeakMap 的鍵,並不會阻止該物件被垃圾回收。一旦用作鍵的物件被回收,它在任何 WeakMap 中的對應值也就成為垃圾回收的候選物件,前提是它們沒有被其他地方強引用。唯一可以用作 WeakMap 鍵的原始型別是 Symbol——更具體地說,是非註冊 Symbol——因為非註冊 Symbol 被保證是唯一的,並且不能被重新建立。
WeakMap 允許以一種不阻止鍵物件被回收的方式將資料與物件關聯起來,即使值引用了鍵。但是,WeakMap 不允許觀察其鍵的存活性,這也是它不允許列舉的原因;如果 WeakMap 暴露任何獲取其鍵列表的方法,該列表將取決於垃圾回收的狀態,從而引入了不確定性。如果你需要鍵的列表,你應該使用 Map 而不是 WeakMap。
你可以在 鍵值集合 指南的 WeakMap 物件 部分了解更多關於 WeakMap 的資訊。
描述
WeakMap 的鍵必須是可被垃圾回收的。大多數 原始資料型別都可以任意建立,並且沒有生命週期,因此不能用作鍵。物件和 非註冊 Symbol 可以用作鍵,因為它們是可被垃圾回收的。
鍵的相等性
與常規 Map 一樣,值相等基於 SameValueZero 演算法,這與 === 運算子相同,因為 WeakMap 只能包含物件和 Symbol 鍵。這意味著對於物件鍵,相等基於物件標識。它們是透過 引用而不是值進行比較的。
為什麼使用 WeakMap?
可以使用兩個陣列(一個用於鍵,一個用於值)來在 JavaScript 中實現一個 Map API,這四個 API 方法共享這兩個陣列。向這個 Map 設定元素需要同時將鍵和值推到這兩個陣列的末尾。結果是,鍵和值的索引將對應於兩個陣列。從 Map 中獲取值需要遍歷所有鍵以查詢匹配項,然後使用該匹配項的索引從值陣列中檢索相應的值。
這樣的實現會有兩個主要的不便之處:
- 第一個是不便之處在於設定和搜尋操作需要 O(n) 的時間複雜度(n 是 Map 中的鍵的數量),因為這兩個操作都必須遍歷鍵列表以查詢匹配的值。
- 第二個不便之處是記憶體洩漏,因為陣列會永久維護對每個鍵和每個值的引用。這些引用會阻止鍵被垃圾回收,即使沒有其他對該物件的引用。這也會阻止相應的值被垃圾回收。
相比之下,在 WeakMap 中,一個鍵物件只要該鍵沒有被垃圾回收,就會對其內容強引用,但之後則弱引用。因此,WeakMap
- 不會阻止垃圾回收,從而最終移除對鍵物件的引用
- 如果鍵物件除了
WeakMap之外沒有其他引用,則允許垃圾回收任何值
當將鍵對映到僅在鍵未被垃圾回收時才具有價值的資訊時,WeakMap 可以是一個特別有用的結構。
但是,由於 WeakMap 不允許觀察其鍵的存活性,因此其鍵是不可列舉的。沒有方法可以獲取鍵的列表。如果存在,列表將取決於垃圾回收的狀態,從而引入了不確定性。如果你需要鍵的列表,你應該使用 Map。
建構函式
WeakMap()-
建立一個新的
WeakMap物件。
例項屬性
這些屬性定義在 WeakMap.prototype 上,並被所有 WeakMap 例項共享。
WeakMap.prototype.constructor-
建立例項物件的建構函式。對於
WeakMap例項,初始值為WeakMap建構函式。 WeakMap.prototype[Symbol.toStringTag]-
[Symbol.toStringTag]屬性的初始值為字串"WeakMap"。此屬性用於Object.prototype.toString()。
例項方法
WeakMap.prototype.delete()-
從此
WeakMap中移除由鍵指定的條目。 WeakMap.prototype.get()-
返回此
WeakMap中與鍵對應的value,如果不存在則返回undefined。 WeakMap.prototype.getOrInsert()實驗性-
返回此
WeakMap中與指定鍵對應的 value。如果鍵不存在,則使用給定的預設值插入新條目,並返回插入的值。 WeakMap.prototype.getOrInsertComputed()實驗性-
返回此
WeakMap中與指定鍵對應的 value。如果鍵不存在,則使用給定回撥函式計算出的預設值插入新條目,並返回插入的值。 WeakMap.prototype.has()-
返回一個布林值,指示此
WeakMap中是否存在具有指定鍵的條目。 WeakMap.prototype.set()-
向此
WeakMap新增具有指定鍵和值的條目,如果鍵已存在,則更新現有條目。
示例
使用 WeakMap
const wm1 = new WeakMap();
const wm2 = new WeakMap();
const wm3 = new WeakMap();
const o1 = {};
const o2 = () => {};
const o3 = window;
wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // a value can be anything, including an object or a function
wm2.set(o2, undefined);
wm2.set(wm1, wm2); // keys and values can be any objects. Even WeakMaps!
wm1.get(o2); // "azerty"
wm2.get(o2); // undefined, because that is the set value
wm2.get(o3); // undefined, because there is no key for o3 on wm2
wm1.has(o2); // true
wm2.has(o2); // true (even if the value itself is 'undefined')
wm2.has(o3); // false
wm3.set(o1, 37);
wm3.get(o1); // 37
wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false
使用 .clear() 方法實現一個類似 WeakMap 的類
class ClearableWeakMap {
#wm;
constructor(init) {
this.#wm = new WeakMap(init);
}
clear() {
this.#wm = new WeakMap();
}
delete(k) {
return this.#wm.delete(k);
}
get(k) {
return this.#wm.get(k);
}
has(k) {
return this.#wm.has(k);
}
set(k, v) {
this.#wm.set(k, v);
return this;
}
}
模擬私有成員
開發人員可以使用 WeakMap 將私有資料與物件關聯起來,具有以下優點:
- 與
Map相比,WeakMap 不持有對用作鍵的物件本身的強引用,因此元資料與物件本身具有相同的生命週期,避免了記憶體洩漏。 - 與使用非列舉和/或
Symbol屬性相比,WeakMap 存在於物件外部,並且使用者程式碼無法透過反射方法(如Object.getOwnPropertySymbols)檢索元資料。 - 與 閉包相比,同一個 WeakMap 可以被同一建構函式建立的所有例項重用,使其更節省記憶體,並允許同一類的不同例項讀取彼此的私有成員。
let Thing;
{
const privateScope = new WeakMap();
let counter = 0;
Thing = function () {
this.someProperty = "foo";
privateScope.set(this, {
hidden: ++counter,
});
};
Thing.prototype.showPublic = function () {
return this.someProperty;
};
Thing.prototype.showPrivate = function () {
return privateScope.get(this).hidden;
};
}
console.log(typeof privateScope);
// "undefined"
const thing = new Thing();
console.log(thing);
// Thing {someProperty: "foo"}
thing.showPublic();
// "foo"
thing.showPrivate();
// 1
這大致相當於使用私有欄位的以下程式碼:
class Thing {
static #counter = 0;
#hidden;
constructor() {
this.someProperty = "foo";
this.#hidden = ++Thing.#counter;
}
showPublic() {
return this.someProperty;
}
showPrivate() {
return this.#hidden;
}
}
console.log(thing);
// Thing {someProperty: "foo"}
thing.showPublic();
// "foo"
thing.showPrivate();
// 1
關聯元資料
WeakMap 可用於將元資料與物件關聯起來,而不會影響物件本身的生命週期。這與私有成員的示例非常相似,因為私有成員也被建模為外部元資料,不參與 原型鏈繼承。
此用例可以擴充套件到已建立的物件。例如,在 Web 上,我們可能希望將額外資料與 DOM 元素關聯起來,DOM 元素稍後可以訪問這些資料。一種常見的方法是將資料附加為屬性:
const buttons = document.querySelectorAll(".button");
buttons.forEach((button) => {
button.clicked = false;
button.addEventListener("click", () => {
button.clicked = true;
const currentButtons = [...document.querySelectorAll(".button")];
if (currentButtons.every((button) => button.clicked)) {
console.log("All buttons have been clicked!");
}
});
});
這種方法有效,但也有一些陷阱:
clicked屬性是可列舉的,因此它會出現在Object.keys(button)、for...in迴圈等中。這可以透過使用Object.defineProperty()來緩解,但這會使程式碼更冗長。clicked屬性是一個普通字串屬性,因此可以被其他程式碼訪問和覆蓋。這可以透過使用Symbol鍵來緩解,但該鍵仍然可以透過Object.getOwnPropertySymbols()訪問。
使用 WeakMap 可以解決這些問題:
const buttons = document.querySelectorAll(".button");
const clicked = new WeakMap();
buttons.forEach((button) => {
clicked.set(button, false);
button.addEventListener("click", () => {
clicked.set(button, true);
const currentButtons = [...document.querySelectorAll(".button")];
if (currentButtons.every((button) => clicked.get(button))) {
console.log("All buttons have been clicked!");
}
});
});
在這裡,只有擁有對 clicked 訪問許可權的程式碼才能知道每個按鈕的點選狀態,並且外部程式碼無法修改狀態。此外,如果任何按鈕從 DOM 中移除,關聯的元資料將自動被垃圾回收。
快取
你可以將傳遞給函式的物件與函式的返回值關聯起來,這樣如果再次傳遞同一個物件,就可以返回快取的結果而無需重新執行函式。如果函式是純函式(即它不修改任何外部物件或引起其他可觀察的副作用),這一點很有用。
const cache = new WeakMap();
function handleObjectValues(obj) {
if (cache.has(obj)) {
return cache.get(obj);
}
const result = Object.values(obj).map(heavyComputation);
cache.set(obj, result);
return result;
}
這隻在函式的輸入是物件時才有效。此外,即使輸入不再被傳遞,只要鍵(輸入)仍然存活,結果就會永遠保留在快取中。更有效的方法是使用一個 Map 與 WeakRef 物件配對,這允許你將任何型別的輸入值與其各自(可能很大)的計算結果關聯起來。有關更多詳細資訊,請參閱 WeakRefs 和 FinalizationRegistry 示例。
規範
| 規範 |
|---|
| ECMAScript® 2026 語言規範 # sec-weakmap-objects |
瀏覽器相容性
載入中…