ES6+ 新特性:Set、Map、WeakSet 與 WeakMap 完全指南
簡介
在傳統的 JavaScript 中,我們常用 Array 來儲存多筆資料、用 Object 來做鍵值對映。但隨著程式規模的成長,這兩種資料結構在 唯一性檢查、鍵的類型限制 以及 記憶體釋放 等方面開始顯得吃力。
ES6(ECMAScript 2015)引入了 Set、Map、WeakSet 與 WeakMap 四種全新的集合類型,讓開發者可以更直觀且效能更佳地處理資料集合與關聯。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,直至實務應用,完整帶你掌握這四個容器的使用方式,適合從初學者到中階開發者閱讀與實作。
核心概念
1. Set – 唯一值的集合
Set 是一種 不允許重複 的資料集合,底層使用 同值相等(SameValueZero) 來判斷是否相同。
- 允許的值類型:任何 JavaScript 值(包括
Object、NaN、-0、+0)。 - 常見方法:
add、delete、has、clear、size(屬性)。
程式碼範例 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轉成陣列,方便後續的陣列方法(如map、filter)使用。
2. Map – 任意鍵值對的集合
Map 類似於 Object,但 鍵(key)可以是任意類型(包括物件、函式),且內建 size、clear、forEach 等方法,遍歷順序亦保留插入順序。
程式碼範例 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 – 只能存放物件且不阻止垃圾回收
WeakSet 與 Set 類似,但 只能儲存物件(object),且對象是 弱引用(Weak Reference),當外部沒有其他引用指向該物件時,垃圾回收機制會自動回收它,WeakSet 內部不會保留該物件。
- 不能直接取得大小 (
size) 或列舉內容(無forEach、keys、values)。 - 只提供
add、delete、has三個方法。
程式碼範例 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 – 鍵為物件且自動釋放
WeakMap 與 Map 互為對應,只是 鍵必須是物件,且同樣採用弱引用。當鍵不再被外部引用時,對應的值會自動被垃圾回收。
- 沒有
size、clear、keys、values、entries,只能用set、get、has、delete。
程式碼範例 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可提供 真正的私有屬性(不被enumerate、Object.keys捕捉),且不會造成記憶體泄漏。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
使用 NaN 判等 |
Set 內部使用 SameValueZero,NaN 被視為相等,=== 則不行。 |
直接使用 Set 去重可正確處理 NaN,不必自行檢查。 |
WeakSet/WeakMap 不能列舉 |
嘗試 for...of 或 Object.keys() 會拋錯。 |
只用於 存在性檢查,或配合 事件回呼 等場景,避免需要遍歷的需求。 |
| 鍵的參考問題 | Map/WeakMap 取值時必須使用「相同參考」的物件。 |
若需要根據「值」而非「參考」查找,可自行實作 Map 的序列化鍵(如 JSON.stringify),或改用 Set/Array 搭配 find。 |
| 記憶體泄漏 | 把大量物件放入 Map 卻忘記在不需要時 delete。 |
使用 WeakMap 代替 Map(鍵為物件)或在適當時機手動 delete。 |
不小心把原始值放入 WeakSet |
會拋 TypeError。 | 確認只加入 object、function,可在 add 前做 typeof 檢查。 |
最佳實踐
- 去重與集合運算:使用
Set搭配展開運算子完成陣列去重、交集、聯集等操作。 - 快取(Cache):將計算結果以 物件作鍵 存入
WeakMap,自動釋放不再使用的快取。 - 私有屬性:利用
WeakMap為類別實例提供真正的私有資料,避免在原型鏈上暴露。 - 事件防重:用
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 帶來的開發快感吧!