這篇文章是 JavaScript MapWeakMap 的下篇,因為篇幅過長所以拆成兩篇,這篇主要介紹 WeakMap 以及他的應用場景。

上篇則是介紹 Map 的使用方法,以及和 Object 之間的差異,有興趣的朋友可以先閱讀上篇:JavaScript 中 Map 與 WeakMap 的介紹,以及和 Object 的差別 (1/2)

什麼是 WeakMap

WeakMapMap 類似,但有兩個主要差異:

  • WeakMap 的 key 只能是 ObjectSymbol
  • WeakMap 的 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 或管理元件狀態時,可能會使用 MapObject 來儲存資料

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 的弱引用特性,就能將它與附帶資料自動清除,從而減少記憶體佔用的風險。

AD

GC 的非決定性與 WeakMap 的限制

使用 WeakMap 協助減少記憶體佔用看起來很好用,但 GC 有一個「非決定性 (non-deterministic)」的特性,也就是說,也就是說,JavaScript 引擎會在其認為適當的時間點執行回收,而非你呼叫 remove() 或變數設為 null 就會馬上清掉。

所以何時能清除回收?這不是我們能控制的。

此外,相對於 MapWeakMap 也有一些限制:

  • 無法遍歷內容:沒有 .keys().values().forEach() 等方法。
  • 無法序列化:跟 Map 不同,他沒有上述的迭代方法,所以他無法直接或間接轉成 JSON。
  • 他的 key 只能是 ObjectSymbol

因此,如果我們需要遍歷資料,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 的資料結構,可以自己用 MapSet 來同步記錄 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));
}

小結

寫了兩篇文章介紹 MapWeakMap,也讓我自己重新再複習一次他們的定義、情境與用法。

  • Map 是用來儲存任意 key 對應任意 value 的結構,key 可以是基本型別或物件,並且支援遍歷與序列操作。它適合用在需要保留資料順序、進行統計、快取、或需要查詢與更新操作的場景。
  • WeakMap 是處理與物件短暫綁定資料的理想選擇,在動態 DOM 操作、元件狀態綁定等場景特別有用。它能有效減輕記憶體壓力,提升前端的穩定性。

以上,有任何問題,都歡迎留言討論唷~。