文章中英模式
布魯斯前端JS面試題目 - Map與Set資料結構
深入解析JavaScript中的Map與Set資料結構,比較Map與Object的差異,探討WeakMap與WeakSet的應用場景,以及面試常見問題解答。
文章中英模式
懶得看文章?那就來看影片吧
Map與Set是什麼?
Map和Set是ES6引入的兩種新型資料結構:
- •
Map
是一種鍵值對的集合,類似於物件,但允許任何類型的值作為鍵 - •
Set
是一種值的集合,其中的每個值都是唯一的
Map的基本用法
// 建立Map
const userMap = new Map();
// 設置鍵值對
userMap.set('name', 'Bruce');
userMap.set(123, 'ID');
// 取得值
console.log(userMap.get('name')); // Bruce
// 檢查鍵是否存在
console.log(userMap.has('name')); // true
// 刪除鍵值對
userMap.delete('name');
Set的基本用法
// 建立Set
const uniqueNumbers = new Set();
// 添加值
uniqueNumbers.add(1);
uniqueNumbers.add(2);
uniqueNumbers.add(1); // 重複值不會被添加
// 檢查值是否存在
console.log(uniqueNumbers.has(1)); // true
// 刪除值
uniqueNumbers.delete(1);
WeakMap與WeakSet
WeakMap和WeakSet是它們的「弱引用」版本,當物件不再被使用時會自動釋放記憶體:
- •只允許物件作為鍵/值
- •不會阻止垃圾回收器回收它們的鍵/值
- •沒有size屬性和清單方法
小遊戲應用範例
// 使用WeakMap儲存關卡暫存資料
const gameCache = new WeakMap();
// 創建關卡 (每關是一個DOM元素)
function createLevel(levelNum) {
const levelDiv = document.createElement('div');
levelDiv.className = 'game-level';
levelDiv.innerHTML = `<h2>第${levelNum}關</h2>`;
// 儲存關卡相關暫存資料
gameCache.set(levelDiv, {
score: 0,
items: ['寶石', '金幣', '武器']
});
return levelDiv;
}
// 切換到下一關
function goToNextLevel() {
// 移除當前關卡DOM元素
const oldLevel = document.querySelector('.game-level');
oldLevel.remove();
// 創建新關卡
const newLevel = createLevel(2);
document.body.appendChild(newLevel);
// 此時舊關卡的暫存資料會自動被回收
// 如果用Map而非WeakMap, 即使關卡DOM被移除
// 暫存資料仍會佔用記憶體, 遊戲久了會變慢或崩潰
Map與Set的內部原理:Hash Table實現
Map和Set在內部都使用Hash Table(雜湊表)實現:
1. Hash函數原理
- •將鍵(或值)轉換為數字索引(雜湊碼)
- •對於原始類型,有專門的雜湊演算法
- •對於物件類型,一般基於記憶體地址
2. Set.add()的重複值處理
- •計算值的雜湊碼
- •檢查該雜湊碼位置是否已有值
- •相同則忽略,不同則添加
// Set處理重複值的例子
const set = new Set();
set.add('a'); // 存儲
set.add('a'); // 已存在,忽略
console.log(set.size); // 1
// 物件比較
const obj1 = {};
const obj2 = {};
set.add(obj1); // 存儲
set.add(obj2); // 不同物件,存儲
console.log(set.size); // 2
Map與Object的區別
特性 | Map | Object |
---|---|---|
鍵的類型 | 任何值 | 只能是字符串或Symbol |
鍵的順序 | 保留插入順序 | 不保證順序 |
Size | 直接獲取 | 需手動計算 |
效能 | 頻繁增刪更優 | 查找可能更快 |
Set中的值比較邏輯
Set使用「Same-value-zero」演算法比較值的唯一性:
- •是
Object.is()
和===
的混合體 - •將
NaN
視為等於自身(像Object.is
) - •但將
+0
和-0
視為相等(像===
) - •物件比較是基於參考位址
const set = new Set();
// NaN 在 Set 中被視為相等
set.add(NaN);
set.add(NaN); // 不會添加
console.log(set.size); // 1
// +0 和 -0 在 Set 中被視為相等
set.add(+0);
set.add(-0); // 不會添加
console.log(set.size); // 仍然是 1
// 物件比較是基於參考
const obj1 = {id: 1};
const obj2 = {id: 1};
set.add(obj1);
set.add(obj2); // 會添加,因為參考不同
console.log(set.size); // 2
set.add(obj1); // 不會添加,參考相同
console.log(set.size); // 仍然是 2
🔥 常見面試題目
(一)如何使用Set去除陣列中的重複元素?
解答:
// 基本用法
const array = [1, 2, 3, 3, 4, 4, 5];
const uniqueArray = [...new Set(array)];
console.log(uniqueArray); // [1, 2, 3, 4, 5]
// 物件陣列去重
const users = [
{ id: 1, name: 'Bruce' },
{ id: 1, name: 'Bruce' },
{ id: 2, name: 'Tony' }
];
const uniqueUsers = Array.from(
new Map(users.map(user => [user.id, user])).values()
);
console.log(uniqueUsers); // 只有兩個物件
這是利用Set不允許重複元素的特性,將陣列轉換為Set後再轉回陣列,是最簡潔高效的去重方法。
(二)Map和Object的主要區別?選擇使用Map的場景?
解答:Map和Object的主要區別在於:
// 1. Map可使用任何類型作為鍵
const map = new Map();
const func = function() {};
map.set(func, 'Function as key');
console.log(map.get(func)); // 'Function as key'
// 2. Map保留插入順序
map.set('first', 1);
map.set('second', 2);
for (let [key, value] of map) {
console.log(key, value); // 按插入順序輸出
}
適合使用Map的場景:
- •當需要非字符串作為鍵時
- •需要頻繁添加和刪除屬性時
- •當鍵值對的順序很重要時
(三)WeakMap和Map的區別?什麼時候應該使用WeakMap?
解答:
// WeakMap用於存儲DOM節點相關數據
const nodeData = new WeakMap();
// 為DOM節點添加數據,不會影響垃圾回收
document.querySelectorAll('button').forEach(node => {
nodeData.set(node, { clickCount: 0 });
});
// 當節點被移除時,相關數據自動被回收
document.body.innerHTML = '';
// 不需要手動清理nodeData
使用WeakMap的場景:
- •需要將額外數據關聯到DOM元素時
- •實現私有變數或狀態時
- •需要避免記憶體洩漏的場合
(四)請解釋HashTable的原理以及Map是如何實現的?
解答:
// HashTable視覺化解釋
/*
Hash表原理示意圖:
鍵值對: { "name": "Bruce", "age": 30, "city": "Taipei", "code": "TW" }
Hash函數 儲存桶(Buckets)
┌─────────┐ ┌───────────────────┐
│ hash() │──── 0 ────────► [ ]
└─────────┘ │ │
└───────────────────┘
┌───────────────────┐
"name" → hash() → 1 │ ┌───────────┐ │
│ │"name":"Bruce"│ │
│ └───────────┘ │
└───────────────────┘
┌───────────────────┐
"city" → hash() → 2 │ ┌───────────┐ │
│ │"city":"Taipei"│ │
│ └───────────┘ │
└───────────────────┘
┌───────────────────────────────┐
"age" → hash() → 3 │ ┌───────────┐ ┌───────────┐ │
"code" → hash() → 3 │ │"age": 30 │ │"code":"TW" │ │ ← 碰撞(collision)!
"job" → hash() → 3 │ └───────────┘ └───────────┘ │ 桶內陣列變長了
└───────────────────────────────┘
*/
// 簡化版HashTable實現
class SimpleHashTable {
constructor() {
this.buckets = Array(4).fill().map(() => []);
}
hash(key) {
return String(key).length % this.buckets.length;
}
set(key, value) {
const index = this.hash(key);
const bucket = this.buckets[index];
const item = bucket.find(item => item.key === key);
if (item) {
item.value = value;
} else {
bucket.push({ key, value });
}
}
get(key) {
const index = this.hash(key);
const bucket = this.buckets[index];
const item = bucket.find(item => item.key === key);
return item ? item.value : undefined;
}
}
// 使用示例 (展示碰撞)
const table = new SimpleHashTable();
// 'name'和'code'都雜湊到索引0
table.set('name', 'Bruce');
table.set('code', 'TW');
// 'age'和'job'都雜湊到索引3
table.set('age', 30);
table.set('job', 'Dev');
// 查看桶的內容
console.log('Bucket 0:', table.buckets[0]);
// 輸出: [
// { key: 'name', value: 'Bruce' },
// { key: 'code', value: 'TW' }
// ]
console.log('Bucket 3:', table.buckets[3]);
// 輸出: [
// { key: 'age', value: 30 },
// { key: 'job', value: 'Dev' }
// ]
實際的Map實現更複雜,包含動態大小調整、高效Hash函數和優化的碰撞處理。碰撞過多時,查找效率會從O(1)退化為O(n),因此現代實現會平衡桶數量與內容。Map和Set的主要操作通常能維持接近O(1)的時間複雜度,這是它們廣泛應用的原因。
(五)Set如何處理重複值?內部機制是什麼?
解答:Set在添加值時會檢查該值是否已存在,使用的是Same-value-zero比較:
// Set處理原始類型重複值
const set = new Set();
// 數字類型
set.add(1);
set.add(1); // 不會添加
console.log(set.size); // 1
// NaN比較
set.add(NaN);
set.add(NaN); // 不會添加
console.log(set.size); // 2
// 物件比較
const obj1 = { id: 1 };
const obj2 = { id: 1 };
set.add(obj1);
set.add(obj2); // 會添加,因為參考不同
console.log(set.size); // 3
set.add(obj1); // 不會添加,參考相同
console.log(set.size); // 3