文章中英模式
布鲁斯前端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