請解釋 Set、Map、WeakSet 和 WeakMap 的區別?

2024年4月8日

💎 加入 E+ 成長計畫 與超過 300+ 位軟體工程師一同在社群中成長,並且獲得更多的軟體工程學習資源

Set

Set 這個數據結構類似陣列,但是裡面的元素值都是唯一,不會有重複的值,無論此值是原始型別 (primitive values) 或引用型別 (object references)。在 JavaScript 當中,Set 本身是一種構造函式,用來生成 Set 這種數據結構,具體的做法是透過 new Set() 來生成實例。

Set 常見操作方法有

  • add(value):新增值至 Set 中
  • delete(value):刪除 Set 中的特定值
  • has(value):檢查 Set 中是否存在特定值
  • size:獲取 Set 中元素的數量

Set 中沒有鍵值(Key),因此使用 entries() 遍歷時,返回的元素將是 [value, value] 的形式:

const set1 = new Set();
set1.add(42);
set1.add("forty two");

const iterator1 = set1.entries();

for (const entry of iterator1) {
  console.log(entry);
  // 預期輸出: [42, 42]
  // 預期輸出: ["forty two", "forty two"]
}

Set 和 WeakSet 的區別

WeakSet 的方法和使用部分與 Set 資料結構相近,本區塊會專注在這兩者不同之處

  • WeakSet 內的元素值只允許是物件(Object),但 Set 可接受各種資料類型的值

    const wSet = new WeakSet();
    const a = [1, 2, 3];
    const b = { name: "explainthis" };
    
    wSet.add(a); // WeakSet {Array(3)}
    wSet.add(b); // WeakSet {{...}}
    wSet.add(1); // Uncaught TypeError: Invalid value used in weak set
    
  • WeakSet 內的元素都是 「弱引用」(weak reference),可以被垃圾回收機制回收。假如使用 Set,即使某個被存入的值,在其他地方已經沒有被引用,該值仍會存在於 Set 當中,不會被垃圾回收。但如果是 WeakSet,則會被垃圾回收。如果要更有意識地做記憶體管理,WeakSet 在許多時候能派上用場。

    const disableElements = new WeakSet();
    const loginButton = document.querySelector("#login");
    
    disableElements.add(loginButton);
    disableElements.has(loginButton); // true
    

Map

類似於 Object 的資料結構,都是用鍵與值 (key-value pair) 的形式儲存資料格式,但還是有許多差異,詳細可以參考這篇 《在 JavaScript 中,Map 與 object 的差別?為什麼有 object 還需要 Map?》一文。Map 本身是一種構造函式,用來生成 Map 這種數據結構,具體做法是 new Map() 來生成實例。

Map 常見操作方法包括:

  • set(key, value):新增元素至 Map 中
  • get(key):通過鍵 (Key) 查詢特定元素並返回
  • has(key):檢查 Map 中是否存在特定鍵 (Key)
  • delete(key):透從 Map 中刪除特定元素
  • size:獲取 Map 中元素的數量

Map 常見遍歷方法 (遍歷順序與元素放入 Map 的順序相同):

  • values():返回 Map 中所有元素的值
  • keys():返回 Map 中所有元素的鍵
  • entries():返回 Map 中所有的元素,返回的會是 [key, value] 的形式

Map 和 WeakMap 的區別

WeakMap 的方法和使用部分與 Map 資料結構相近,但有以下區別:

  • WeakMap 中的鍵名 (Key) 只能是物件 (Object) 和 Symbol,不接受其他資料類型作為鍵名,例如原始值 (primitive values) 如字串、數字、布林值等,但不包括 null。相比之下,Map 可以接受各種資料類型作為鍵名 (Key)。

  • WeakMap 中的鍵名是「弱引用」(weak reference),鍵名 (key) 所指向的對象可以被垃圾回收,此時的鍵名 (key) 是無效的

// 如果放入的物件在外面沒有其他引用,在 WeakMap 中會被垃圾回收掉
let food = new WeakMap();
let fruit = { name: "apple" };

food.set(fruit, "good"); // 將 fruit 物件置入 WeakMap 中
fruit = null; // 移除 fruit 的引用
console.log(food);
// 因為 JavaScript 的垃圾回收時機會因為不同引擎而有差異,所以可能不會馬上被回收,以上可能 log 出兩種情境
// WeakMap {Object => "good"},fruit 的引用被移除,但物件可能還未被垃圾回收
// WeakMap(0) fruit 已經被垃圾回收,因此 WeakMap 中沒有項目

// 一般的 Map,即使放入的物件在外面沒有其他引用,仍在 Map 當中存放
let food = new Map();
let fruit = { name: "apple" };

food.set(fruit, "good");
console.log(food); // Map(1)

fruit = null;
console.log(food); // Map(1) fruit 不會被垃圾回收

在上方的強引用程式碼中,雖然 fruit 物件最後被重新賦值為 null (意思等同於無法再透過 fruit 變數獲取該對象值,因為其中的引用被斷開),但由於 food 與此物件間存在強引用,所以被保留在記憶體中,這就是前面提到的,強引用會防止物件被垃圾回收,並將物件保留在記憶體當中; 弱引用則相反,並不能防止物件被垃圾回收,當 JavaScript 執行環境執行垃圾回收時,上述弱引用例子中的 fruit 物件會被從記憶體和 WeakMap 中刪除。

弱引用的適用情境在於,如果引用的物件在未來可能會被刪除的情況、且不想防止被垃圾回收時,就適合用 WeakMap 或 WeakSet。例如,如果我們想要記錄一些與 DOM 節點相關的數據,有一種方法是使用 Expando 擴充節點上的資訊,但壞處是會直接修改到這個 DOM 節點、且如果未來這個節點被移除時,相關資訊不會被垃圾回收掉,這時如果是使用 WeakMap 就會是很好的替代方案。

備註:如果直接將弱引用程式碼的例子在 JavaScript 執行環境中執行,可能還是會看到 WeakMap 中有值,這是因為 JavaScript 執行環境會在特定的時間點執行垃圾回收。

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們