本文 AI 產出,尚未審核

ES6+ 新特性:Set、Map、WeakSet 與 WeakMap 完全指南


簡介

在傳統的 JavaScript 中,我們常用 Array 來儲存多筆資料、用 Object 來做鍵值對映。但隨著程式規模的成長,這兩種資料結構在 唯一性檢查鍵的類型限制 以及 記憶體釋放 等方面開始顯得吃力。
ES6(ECMAScript 2015)引入了 SetMapWeakSetWeakMap 四種全新的集合類型,讓開發者可以更直觀且效能更佳地處理資料集合與關聯。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,直至實務應用,完整帶你掌握這四個容器的使用方式,適合從初學者到中階開發者閱讀與實作。


核心概念

1. Set – 唯一值的集合

Set 是一種 不允許重複 的資料集合,底層使用 同值相等(SameValueZero) 來判斷是否相同。

  • 允許的值類型:任何 JavaScript 值(包括 ObjectNaN-0+0)。
  • 常見方法:adddeletehasclearsize(屬性)。

程式碼範例 1:基本操作

// 建立 Set,並自動去除重複元素
const numbers = new Set([1, 2, 2, 3, 4, 4, 5]);
console.log(numbers); // Set(5) {1, 2, 3, 4, 5}

// 新增元素,若已存在則不會重複
numbers.add(3);        // 不會再次加入
numbers.add(6);
console.log([...numbers]); // [1, 2, 3, 4, 5, 6]

// 判斷是否包含某個值
console.log(numbers.has(2)); // true
console.log(numbers.has(10)); // false

// 刪除與清空
numbers.delete(1);
numbers.clear(); // 移除所有元素
console.log(numbers.size); // 0

小技巧:使用展開運算子 ...Array.from() 可快速把 Set 轉成陣列,方便後續的陣列方法(如 mapfilter)使用。

2. Map – 任意鍵值對的集合

Map 類似於 Object,但 鍵(key)可以是任意類型(包括物件、函式),且內建 sizeclearforEach 等方法,遍歷順序亦保留插入順序。

程式碼範例 2:Map 的 CRUD

// 建立 Map
const userRoles = new Map();

// 使用任意類型作為鍵
const admin = { id: 1, name: 'Alice' };
const guest = { id: 2, name: 'Bob' };

userRoles.set(admin, 'admin');
userRoles.set(guest, 'guest');
userRoles.set('system', 'root'); // 也可以使用字串作鍵

// 取值
console.log(userRoles.get(admin)); // 'admin'
console.log(userRoles.get('system')); // 'root'

// 判斷鍵是否存在
console.log(userRoles.has(guest)); // true

// 迭代 Map(保留插入順序)
for (const [key, value] of userRoles) {
  console.log(key, '=>', value);
}

/* 輸出:
{ id: 1, name: 'Alice' } => admin
{ id: 2, name: 'Bob' } => guest
system => root
*/

// 刪除與清空
userRoles.delete(admin);
userRoles.clear();
console.log(userRoles.size); // 0

重點Map.prototype.get 只接受同一個參考的鍵,若使用相同結構但不同參考的物件,會回傳 undefined

3. WeakSet – 只能存放物件且不阻止垃圾回收

WeakSetSet 類似,但 只能儲存物件(object),且對象是 弱引用(Weak Reference),當外部沒有其他引用指向該物件時,垃圾回收機制會自動回收它,WeakSet 內部不會保留該物件。

  • 不能直接取得大小 (size) 或列舉內容(無 forEachkeysvalues)。
  • 只提供 adddeletehas 三個方法。

程式碼範例 3:WeakSet 的典型用途 – 防止重複執行

// 假設有一個需要避免重複執行的任務
const runningTasks = new WeakSet();

function startTask(taskObj) {
  if (runningTasks.has(taskObj)) {
    console.warn('Task already running');
    return;
  }
  runningTasks.add(taskObj);
  // 模擬非同步工作
  setTimeout(() => {
    console.log('Task finished');
    runningTasks.delete(taskObj); // 任務結束後移除
  }, 1000);
}

// 使用範例
const taskA = { id: 'A' };
startTask(taskA); // 正常執行
startTask(taskA); // 會被阻止

// 當外部不再持有 taskA 的引用,GC 會自動回收,WeakSet 不會阻礙

注意:因為無法列舉,WeakSet 主要用於 「是否已經加入」的快速檢查,例如 防止重複事件綁定避免多次執行同一個物件任務

4. WeakMap – 鍵為物件且自動釋放

WeakMapMap 互為對應,只是 鍵必須是物件,且同樣採用弱引用。當鍵不再被外部引用時,對應的值會自動被垃圾回收。

  • 沒有 sizeclearkeysvaluesentries,只能用 setgethasdelete

程式碼範例 4:WeakMap 作為私有屬性儲存

// 建立一個 WeakMap 來存放每個物件的隱私資料
const privateData = new WeakMap();

class Person {
  constructor(name, age) {
    // 把隱私資料放入 WeakMap,外部無法直接存取
    privateData.set(this, { name, age });
  }

  getInfo() {
    const data = privateData.get(this);
    return `${data.name} (${data.age} 歲)`;
  }
}

let alice = new Person('Alice', 30);
console.log(alice.getInfo()); // Alice (30 歲)

// 當 alice 變成 null,對應的私有資料會自動被回收
alice = null;
// 此時 privateData 內已不再保留該物件的資料(GC 會自行處理)

實務意義:在 模組化或類別封裝 時,WeakMap 可提供 真正的私有屬性(不被 enumerateObject.keys 捕捉),且不會造成記憶體泄漏。


常見陷阱與最佳實踐

陷阱 說明 解決方式
使用 NaN 判等 Set 內部使用 SameValueZero,NaN 被視為相等,=== 則不行。 直接使用 Set 去重可正確處理 NaN,不必自行檢查。
WeakSet/WeakMap 不能列舉 嘗試 for...ofObject.keys() 會拋錯。 只用於 存在性檢查,或配合 事件回呼 等場景,避免需要遍歷的需求。
鍵的參考問題 Map/WeakMap 取值時必須使用「相同參考」的物件。 若需要根據「值」而非「參考」查找,可自行實作 Map 的序列化鍵(如 JSON.stringify),或改用 Set/Array 搭配 find
記憶體泄漏 把大量物件放入 Map 卻忘記在不需要時 delete 使用 WeakMap 代替 Map(鍵為物件)或在適當時機手動 delete
不小心把原始值放入 WeakSet 會拋 TypeError。 確認只加入 objectfunction,可在 add 前做 typeof 檢查。

最佳實踐

  1. 去重與集合運算:使用 Set 搭配展開運算子完成陣列去重、交集、聯集等操作。
  2. 快取(Cache):將計算結果以 物件作鍵 存入 WeakMap,自動釋放不再使用的快取。
  3. 私有屬性:利用 WeakMap 為類別實例提供真正的私有資料,避免在原型鏈上暴露。
  4. 事件防重:用 WeakSet 記錄已綁定的 DOM 元素,防止重複綁定事件監聽器。

實際應用場景

1. 表單驗證:利用 Set 產生唯一的錯誤代碼集合

function validate(form) {
  const errors = new Set();

  if (!form.email) errors.add('EMAIL_REQUIRED');
  if (!/^\S+@\S+\.\S+$/.test(form.email)) errors.add('EMAIL_INVALID');
  if (form.password.length < 8) errors.add('PWD_TOO_SHORT');

  return [...errors]; // 直接回傳唯一錯誤代碼陣列
}

2. 記憶體友善的物件快取:WeakMap 作為圖形渲染的緩存

const textureCache = new WeakMap();

function getTexture(image) {
  if (textureCache.has(image)) return textureCache.get(image);
  const tex = createWebGLTexture(image); // 假設的耗時函式
  textureCache.set(image, tex);
  return tex;
}

只要 image 物件在其他地方不再被引用,對應的 tex 會自動被回收,避免手動清除快取的繁瑣。

3. 防止重複執行的 UI 動畫:WeakSet 記錄已執行的元素

const animatedElems = new WeakSet();

function animate(elem) {
  if (animatedElems.has(elem)) return; // 已經在動畫中
  animatedElems.add(elem);
  elem.classList.add('fade-in');
  elem.addEventListener('animationend', () => {
    animatedElems.delete(elem);
  });
}

4. 角色權限對照表:Map 取代嵌套物件

const rolePermissions = new Map([
  ['admin',   new Set(['read', 'write', 'delete'])],
  ['editor',  new Set(['read', 'write'])],
  ['viewer',  new Set(['read'])]
]);

function can(userRole, action) {
  const perms = rolePermissions.get(userRole);
  return perms ? perms.has(action) : false;
}

總結

  • Set 提供唯一值集合,適合去重、交集/聯集等集合運算。
  • Map 允許任意類型作鍵,保持插入順序,常用於快取、關聯資料表。
  • WeakSet 僅接受物件鍵且不阻止 GC,適合 存在性檢查(如防止重複事件)。
  • WeakMap 以物件作鍵、弱引用保存值,為 私有屬性記憶體友善快取 提供理想解法。

掌握這四個容器後,你可以寫出 更簡潔、更高效、更安全 的 JavaScript 程式碼,尤其在大型前端專案或 Node.js 後端服務中,正確選用集合類型能顯著降低記憶體使用與程式錯誤。立即在你的專案裡試試看,體驗 Modern JS 帶來的開發快感吧!