跳至主要內容

[Medium] Set & Map

1. What are Set and Map?

什麼是 Set 和 Map?

SetMap 是 ES6 引入的兩種新的資料結構,它們提供了比傳統物件和陣列更適合特定場景的解決方案。

Set(集合)

定義Set 是一種值唯一的集合,類似於數學中的集合概念。

特點

  • 儲存的值不會重複
  • 使用 === 來判斷值是否相等
  • 保持插入順序
  • 可以儲存任何類型的值(原始型別或物件)

基本用法

// 建立 Set
const set = new Set();

// 新增值
set.add(1);
set.add(2);
set.add(2); // 重複值不會被加入
set.add('hello');
set.add({ name: 'John' });

console.log(set.size); // 4
console.log(set); // Set(4) { 1, 2, 'hello', { name: 'John' } }

// 檢查值是否存在
console.log(set.has(1)); // true
console.log(set.has(3)); // false

// 刪除值
set.delete(2);
console.log(set.has(2)); // false

// 清空 Set
set.clear();
console.log(set.size); // 0

從陣列建立 Set

// 去除陣列中的重複值
const arr = [1, 2, 2, 3, 3, 3];
const uniqueSet = new Set(arr);
console.log(uniqueSet); // Set(3) { 1, 2, 3 }

// 轉回陣列
const uniqueArr = [...uniqueSet];
console.log(uniqueArr); // [1, 2, 3]

// 簡寫
const uniqueArr2 = [...new Set(arr)];

Map(映射)

定義Map 是一種鍵值對的集合,類似於物件,但鍵可以是任何類型。

特點

  • 鍵可以是任何類型(字串、數字、物件、函式等)
  • 保持插入順序
  • size 屬性
  • 迭代順序與插入順序一致

基本用法

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

// 新增鍵值對
map.set('name', 'John');
map.set(1, 'one');
map.set(true, 'boolean');
map.set({ id: 1 }, 'object key');

// 取得值
console.log(map.get('name')); // 'John'
console.log(map.get(1)); // 'one'

// 檢查鍵是否存在
console.log(map.has('name')); // true

// 刪除鍵值對
map.delete('name');

// 取得大小
console.log(map.size); // 3

// 清空 Map
map.clear();

從陣列建立 Map

// 從二維陣列建立
const entries = [
['name', 'John'],
['age', 30],
['city', 'Taipei'],
];
const map = new Map(entries);
console.log(map.get('name')); // 'John'

// 從物件建立
const obj = { name: 'John', age: 30 };
const map2 = new Map(Object.entries(obj));
console.log(map2.get('name')); // 'John'

2. Set vs Array

Set 與陣列的差異

特性SetArray
重複值不允許允許
索引存取不支援支援
查找效能O(1)O(n)
插入順序保持保持
常用方法add, has, deletepush, pop, indexOf

使用場景

// ✅ 適合使用 Set:需要唯一值
const userIds = new Set([1, 2, 3, 2, 1]);
console.log([...userIds]); // [1, 2, 3]

// ✅ 適合使用 Set:快速檢查存在性
const visitedPages = new Set();
visitedPages.add('/home');
visitedPages.add('/about');
if (visitedPages.has('/home')) {
console.log('已訪問過首頁');
}

// ✅ 適合使用 Array:需要索引或重複值
const scores = [100, 95, 100, 90]; // 允許重複
console.log(scores[0]); // 100

3. Map vs Object

Map 與物件的差異

特性MapObject
鍵的類型任何類型字串或 Symbol
大小size 屬性需手動計算
預設鍵有原型鏈
迭代順序插入順序ES2015+ 保持插入順序
效能頻繁增刪較快一般情況較快
JSON不直接支援原生支援

使用場景

// ✅ 適合使用 Map:鍵不是字串
const userMetadata = new Map();
const user1 = { id: 1 };
const user2 = { id: 2 };

userMetadata.set(user1, { lastLogin: '2024-01-01' });
userMetadata.set(user2, { lastLogin: '2024-01-02' });

console.log(userMetadata.get(user1)); // { lastLogin: '2024-01-01' }

// ✅ 適合使用 Map:需要頻繁增刪
const cache = new Map();
cache.set('key1', 'value1');
cache.delete('key1');
cache.set('key2', 'value2');

// ✅ 適合使用 Object:靜態結構、需要 JSON
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
};
const json = JSON.stringify(config); // 可直接序列化

4. Common Interview Questions

常見面試題目

題目 1:去除陣列重複值

請實作一個函式,去除陣列中的重複值。

function removeDuplicates(arr) {
// 你的實作
}
點擊查看答案

方法 1:使用 Set(推薦)

function removeDuplicates(arr) {
return [...new Set(arr)];
}

console.log(removeDuplicates([1, 2, 2, 3, 3, 3])); // [1, 2, 3]
console.log(removeDuplicates(['a', 'b', 'a', 'c'])); // ['a', 'b', 'c']

方法 2:使用 filter + indexOf

function removeDuplicates(arr) {
return arr.filter((value, index) => arr.indexOf(value) === index);
}

方法 3:使用 reduce

function removeDuplicates(arr) {
return arr.reduce((acc, value) => {
if (!acc.includes(value)) {
acc.push(value);
}
return acc;
}, []);
}

效能比較

  • Set 方法:O(n),最快
  • filter + indexOf:O(n²),較慢
  • reduce + includes:O(n²),較慢

題目 2:檢查陣列是否有重複值

請實作一個函式,檢查陣列中是否有重複值。

function hasDuplicates(arr) {
// 你的實作
}
點擊查看答案

方法 1:使用 Set(推薦)

function hasDuplicates(arr) {
return new Set(arr).size !== arr.length;
}

console.log(hasDuplicates([1, 2, 3])); // false
console.log(hasDuplicates([1, 2, 2, 3])); // true

方法 2:使用 Set 的 has 方法

function hasDuplicates(arr) {
const seen = new Set();
for (const value of arr) {
if (seen.has(value)) {
return true;
}
seen.add(value);
}
return false;
}

方法 3:使用 indexOf

function hasDuplicates(arr) {
return arr.some((value, index) => arr.indexOf(value) !== index);
}

效能比較

  • Set 方法 1:O(n),最快
  • Set 方法 2:O(n),平均情況下可能提前結束
  • indexOf 方法:O(n²),較慢

題目 3:計算元素出現次數

請實作一個函式,計算陣列中每個元素出現的次數。

function countOccurrences(arr) {
// 你的實作
}
點擊查看答案

方法 1:使用 Map(推薦)

function countOccurrences(arr) {
const map = new Map();

for (const value of arr) {
map.set(value, (map.get(value) || 0) + 1);
}

return map;
}

const arr = ['a', 'b', 'a', 'c', 'b', 'a'];
const counts = countOccurrences(arr);
console.log(counts.get('a')); // 3
console.log(counts.get('b')); // 2
console.log(counts.get('c')); // 1

方法 2:使用 reduce + Map

function countOccurrences(arr) {
return arr.reduce((map, value) => {
map.set(value, (map.get(value) || 0) + 1);
return map;
}, new Map());
}

方法 3:轉換為物件

function countOccurrences(arr) {
const counts = {};
for (const value of arr) {
counts[value] = (counts[value] || 0) + 1;
}
return counts;
}

const arr = ['a', 'b', 'a', 'c', 'b', 'a'];
const counts = countOccurrences(arr);
console.log(counts); // { a: 3, b: 2, c: 1 }

使用 Map 的優勢

  • 鍵可以是任何類型(物件、函式等)
  • size 屬性
  • 迭代順序與插入順序一致

題目 4:找出兩個陣列的交集

請實作一個函式,找出兩個陣列的交集(共同元素)。

function intersection(arr1, arr2) {
// 你的實作
}
點擊查看答案

方法 1:使用 Set

function intersection(arr1, arr2) {
const set1 = new Set(arr1);
const set2 = new Set(arr2);
const result = [];

for (const value of set1) {
if (set2.has(value)) {
result.push(value);
}
}

return result;
}

console.log(intersection([1, 2, 3], [2, 3, 4])); // [2, 3]

方法 2:使用 filter + Set

function intersection(arr1, arr2) {
const set2 = new Set(arr2);
return [...new Set(arr1)].filter((value) => set2.has(value));
}

方法 3:使用 filter + includes

function intersection(arr1, arr2) {
return arr1.filter((value) => arr2.includes(value));
}

效能比較

  • Set 方法:O(n + m),最快
  • filter + includes:O(n × m),較慢

題目 5:找出兩個陣列的差集

請實作一個函式,找出兩個陣列的差集(在 arr1 中但不在 arr2 中的元素)。

function difference(arr1, arr2) {
// 你的實作
}
點擊查看答案

方法 1:使用 Set

function difference(arr1, arr2) {
const set2 = new Set(arr2);
return arr1.filter((value) => !set2.has(value));
}

console.log(difference([1, 2, 3, 4], [2, 3])); // [1, 4]

方法 2:使用 Set 去重後再過濾

function difference(arr1, arr2) {
const set1 = new Set(arr1);
const set2 = new Set(arr2);
return [...set1].filter((value) => !set2.has(value));
}

方法 3:使用 includes

function difference(arr1, arr2) {
return arr1.filter((value) => !arr2.includes(value));
}

效能比較

  • Set 方法:O(n + m),最快
  • includes 方法:O(n × m),較慢

題目 6:實作 LRU Cache

請使用 Map 實作一個 LRU(Least Recently Used)快取。

class LRUCache {
constructor(capacity) {
// 你的實作
}

get(key) {
// 你的實作
}

put(key, value) {
// 你的實作
}
}
點擊查看答案

實作

class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}

get(key) {
if (!this.cache.has(key)) {
return -1;
}

// 將該鍵移到最後(表示最近使用)
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);

return value;
}

put(key, value) {
// 如果鍵已存在,先刪除
if (this.cache.has(key)) {
this.cache.delete(key);
}
// 如果容量已滿,刪除最舊的鍵(第一個)
else if (this.cache.size >= this.capacity) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}

// 新增鍵值對(會自動放在最後)
this.cache.set(key, value);
}
}

// 使用範例
const cache = new LRUCache(2);
cache.put(1, 'one');
cache.put(2, 'two');
console.log(cache.get(1)); // 'one'
cache.put(3, 'three'); // 移除鍵 2
console.log(cache.get(2)); // -1(已被移除)
console.log(cache.get(3)); // 'three'

說明

  • Map 保持插入順序,第一個鍵是最舊的
  • get 時將鍵移到最後,表示最近使用
  • put 時如果容量已滿,刪除第一個鍵

題目 7:使用物件作為 Map 的鍵

請說明以下程式碼的輸出結果。

const map = new Map();
const obj1 = { id: 1 };
const obj2 = { id: 1 };

map.set(obj1, 'first');
map.set(obj2, 'second');

console.log(map.get(obj1));
console.log(map.get(obj2));
console.log(map.size);
點擊查看答案
// 'first'
// 'second'
// 2

解釋

  • obj1obj2 雖然內容相同,但是不同的物件實例
  • Map 使用引用比較(reference comparison),不是值比較
  • 因此 obj1obj2 被視為不同的鍵
  • 如果使用普通物件作為 Map,會將物件轉為字串 [object Object],導致所有物件都變成同一個鍵

對比普通物件

// 普通物件:鍵會被轉為字串
const obj = {};
const obj1 = { id: 1 };
const obj2 = { id: 1 };

obj[obj1] = 'first';
obj[obj2] = 'second';

console.log(obj[obj1]); // 'second'(被覆蓋)
console.log(obj[obj2]); // 'second'
console.log(Object.keys(obj)); // ['[object Object]'](只有一個鍵)

// Map:保持物件引用
const map = new Map();
map.set(obj1, 'first');
map.set(obj2, 'second');

console.log(map.get(obj1)); // 'first'
console.log(map.get(obj2)); // 'second'
console.log(map.size); // 2

5. WeakSet 與 WeakMap

WeakSet 與 WeakMap 的差異

WeakSet

特點

  • 只能儲存物件(不能儲存原始型別)
  • 弱引用:如果物件沒有其他引用,會被垃圾回收
  • 沒有 size 屬性
  • 不可迭代
  • 沒有 clear 方法

使用場景:標記物件,避免記憶體洩漏

const weakSet = new WeakSet();

const obj1 = { id: 1 };
const obj2 = { id: 2 };

weakSet.add(obj1);
weakSet.add(obj2);

console.log(weakSet.has(obj1)); // true

// 當 obj1 沒有其他引用時,會被垃圾回收
// weakSet 中的引用也會自動清除

WeakMap

特點

  • 鍵只能是物件(不能是原始型別)
  • 弱引用:如果鍵物件沒有其他引用,會被垃圾回收
  • 沒有 size 屬性
  • 不可迭代
  • 沒有 clear 方法

使用場景:儲存物件的私有資料,避免記憶體洩漏

const weakMap = new WeakMap();

const obj1 = { id: 1 };
const obj2 = { id: 2 };

weakMap.set(obj1, 'data1');
weakMap.set(obj2, 'data2');

console.log(weakMap.get(obj1)); // 'data1'

// 當 obj1 沒有其他引用時,會被垃圾回收
// weakMap 中的鍵值對也會自動清除

WeakSet/WeakMap vs Set/Map 比較

特性Set/MapWeakSet/WeakMap
鍵/值類型任何類型只能是物件
弱引用
可迭代
size 屬性
clear 方法
垃圾回收不會自動清除會自動清除

6. Best Practices

最佳實踐

推薦做法

// 1. 需要唯一值時使用 Set
const uniqueIds = new Set([1, 2, 3, 2, 1]);
console.log([...uniqueIds]); // [1, 2, 3]

// 2. 需要快速查找時使用 Set
const allowedUsers = new Set(['user1', 'user2', 'user3']);
if (allowedUsers.has(currentUser)) {
// 允許訪問
}

// 3. 鍵不是字串時使用 Map
const metadata = new Map();
const user = { id: 1 };
metadata.set(user, { lastLogin: new Date() });

// 4. 需要頻繁增刪時使用 Map
const cache = new Map();
cache.set('key', 'value');
cache.delete('key');

// 5. 需要關聯物件資料且避免記憶體洩漏時使用 WeakMap
const privateData = new WeakMap();
class User {
constructor(name) {
privateData.set(this, { name });
}
getName() {
return privateData.get(this).name;
}
}

避免的做法

// 1. 不要用 Set 來取代陣列的所有功能
// ❌ 不好:需要索引時仍用 Set
const set = new Set([1, 2, 3]);
// set[0] // undefined,無法用索引存取

// ✅ 好:需要索引時用陣列
const arr = [1, 2, 3];
arr[0]; // 1

// 2. 不要用 Map 來取代物件的所有功能
// ❌ 不好:簡單的靜態結構用 Map
const config = new Map();
config.set('apiUrl', 'https://api.example.com');
config.set('timeout', 5000);

// ✅ 好:簡單結構用物件
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
};

// 3. 不要混淆 Set 和 Map
// ❌ 錯誤:Set 沒有鍵值對
const set = new Set();
set.set('key', 'value'); // TypeError: set.set is not a function

// ✅ 正確:Map 才有鍵值對
const map = new Map();
map.set('key', 'value');

7. Interview Summary

面試總結

快速記憶

Set(集合)

  • 值唯一,不重複
  • 快速查找:O(1)
  • 適合:去重、快速檢查存在性

Map(映射)

  • 鍵值對,鍵可以是任何類型
  • 保持插入順序
  • 適合:非字串鍵、頻繁增刪

WeakSet/WeakMap

  • 弱引用,自動垃圾回收
  • 鍵/值只能是物件
  • 適合:避免記憶體洩漏

面試回答範例

Q: 什麼時候應該使用 Set 而不是陣列?

"當需要確保值唯一或需要快速檢查值是否存在時,應該使用 Set。Set 的 has 方法是 O(1) 時間複雜度,而陣列的 includes 是 O(n)。例如去除陣列重複值或檢查使用者權限時,Set 會更有效率。"

Q: Map 和物件的差異是什麼?

"Map 的鍵可以是任何類型,包括物件、函式等,而物件的鍵只能是字串或 Symbol。Map 有 size 屬性可以直接取得大小,而物件需要手動計算。Map 保持插入順序,且沒有原型鏈,適合儲存純資料。當需要將物件作為鍵或需要頻繁增刪時,Map 是更好的選擇。"

Q: WeakMap 和 Map 的差異是什麼?

"WeakMap 的鍵只能是物件,且使用弱引用。當鍵物件沒有其他引用時,WeakMap 中的對應條目會被自動垃圾回收,這可以避免記憶體洩漏。WeakMap 不可迭代,也沒有 size 屬性。適合用於儲存物件的私有資料或元資料,當物件被銷毀時,相關資料也會自動清除。"

Reference