這篇文章是 JavaScript Map 與 WeakMap 的下篇,因為篇幅過長所以拆成兩篇,這篇主要介紹 WeakMap 以及他的應用場景。
上篇則是介紹 Map 的使用方法,以及和 Object 之間的差異,有興趣的朋友可以先閱讀上篇:JavaScript 中 Map 與 WeakMap 的介紹,以及和 Object 的差別 (1/2)
什麼是 WeakMap
WeakMap 和 Map 類似,但有兩個主要差異:
WeakMap的 key 只能是Object和SymbolWeakMap的 key 是弱引用,不會阻止 JavaScript 的垃圾回收機制 (Garbage Collector, GC) 將物件清除,且一旦 key 被回收,對應的值也會自動從WeakMap中移除
const wm = new WeakMap();
const obj = {};
wm.set(obj, { info: "data" });
// 當 obj 不再被引用時,GC 會將其清除,WeakMap 自動釋放對應值垃圾回收機制 (Garbage Collector) 是運行環境中的一個機制,負責自動管理記憶體。它會找出程式中不再使用的物件或資料 (例如沒有任何變數或引用指向它們),然後釋放這些資料佔用的記憶體空間,讓系統可以重新利用這部分記憶體。這樣可以避免記憶體洩漏,確保程式運行時不會因為記憶體用盡而崩潰。
▼ 在 JavaScript 中,垃圾回收機制是引擎的一部分,會在背景默默運作。例如:
let obj = { name: 'test' }; // 建立一個物件
obj = null; // 移除對物件的引用
// 此時,垃圾回收器會發現這個物件不再被使用,稍後會回收它簡單來說,垃圾回收機制就像一個「記憶體清潔工」,幫程式清理用不到的東西。
WeakMap 的優勢與記憶體洩漏有關
▼ 我們開發專案時,如果需要動態操作 DOM 或管理元件狀態時,可能會使用 Map 或 Object 來儲存資料
const data = new Map();
const el = document.querySelector('#myElement');
data.set(el, { selected: true });
el.remove();
雖然用了 el.remove 將他從 DOM 中移除,但只要 data 還有對它的引用,該 el 一樣會存在於記憶體中,導致無法被 GC 清除,這就是記憶體洩漏的原因之一。
▼ 此時可以改用 WeakMap 處理
const data = new WeakMap();
const el = document.querySelector('#myElement');
data.set(el, { selected: true });
el.remove(); // 若無其他引用,el 將被自動回收
改用 WeakMap,一旦元素 (el) 不再被其他地方引用時,根據 WeakMap 的弱引用特性,就能將它與附帶資料自動清除,從而減少記憶體佔用的風險。
GC 的非決定性與 WeakMap 的限制
使用 WeakMap 協助減少記憶體佔用看起來很好用,但 GC 有一個「非決定性 (non-deterministic)」的特性,也就是說,也就是說,JavaScript 引擎會在其認為適當的時間點執行回收,而非你呼叫 remove() 或變數設為 null 就會馬上清掉。
所以何時能清除回收?這不是我們能控制的。
此外,相對於 Map,WeakMap 也有一些限制:
- 無法遍歷內容:沒有
.keys()、.values()、.forEach()等方法。 - 無法序列化:跟
Map不同,他沒有上述的迭代方法,所以他無法直接或間接轉成 JSON。 - 他的 key 只能是
Object和Symbol。
因此,如果我們需要遍歷資料,WeakMap 並不是適合的選擇。它的強項在於需要動態操作 DOM 或關聯特定物件資料時,能避免記憶體洩漏的問題。
範例:用 WeakMap 追蹤表單欄位
一樣以表單為例子,這次的情境是表單欄位的資料綁定。
▼ 假設我們需要加減輸入框,並且要存每個欄位的驗證規則和錯誤訊息
// 初始化 WeakMap
const fieldData = new WeakMap();
// 註冊欄位
const registerField = (inputEl) => {
fieldData.set(inputEl, {
validate: () => inputEl.value.length > 3,
error: null,
});
}
// 欄位改變時
const handleInputChange = (inputEl) => {
const meta = fieldData.get(inputEl);
if (meta && !meta.validate()) {
meta.error = "輸入太短";
}
}
const input = document.createElement("input");
document.body.appendChild(input);
registerField(input);
// 模擬移除欄位
document.getElementById("remove").addEventListener("click", () => {
input.remove(); // 元素與其狀態資料將自動清除
console.log("欄位已移除,WeakMap 中的資料也會自動清除(無法再存取該 key)");
});這樣的設計,可以讓欄位資料的生命週期與 DOM 元素緊密同步,我們就不用手動清除,也不會發生遺留記憶體的問題。
為什麼不能直接列印 WeakMap?
如果你有使用上面的範例,可能會想印出 fieldData 現在的資料結構,但 WeakMap 的設計是不能被遍歷,也不能被印出內容的,他是用來保護 key 不會被意外存取,為的是讓 GC 可以正常運作。
// 你只能看到 [object WeakMap],看不到內容
console.log(fieldData);
// 會報錯
for (let [key, value] of fieldData) { ... }▼ 如果我們堅持想看 fieldData 的資料結構,可以自己用 Map 或 Set 來同步記錄 key,就能間接印出內容
const fieldData = new WeakMap();
const testData = new Set();
// 建立 key
const key1 = {};
// 新增 key-value 到 WeakMap,並追蹤這個 key
fieldData.set(key1, 'value1');
testData.add(key1);
// 使用 testData 印出 fieldData 的 key & value
for (const key of testData) {
console.log('key:', key, 'value:', fieldData.get(key));
}小結
寫了兩篇文章介紹 Map 和 WeakMap,也讓我自己重新再複習一次他們的定義、情境與用法。
Map是用來儲存任意 key 對應任意 value 的結構,key 可以是基本型別或物件,並且支援遍歷與序列操作。它適合用在需要保留資料順序、進行統計、快取、或需要查詢與更新操作的場景。WeakMap是處理與物件短暫綁定資料的理想選擇,在動態 DOM 操作、元件狀態綁定等場景特別有用。它能有效減輕記憶體壓力,提升前端的穩定性。
以上,有任何問題,都歡迎留言討論唷~。