26. 说说你对Set和Map的理解?
问题解析
Set 和 Map 是 ES6 引入的两种新的数据结构,它们为 JavaScript 提供了更高效的集合操作能力。面试中通常会考察它们的基本用法、特点、与普通对象的区别以及应用场景。
核心概念
1. Set
Set 是一种无序且唯一的数据集合,类似于数组,但成员的值都是唯一的,没有重复。
// 创建 Set
const set = new Set();
// 通过数组创建
const set2 = new Set([1, 2, 3, 4, 4, 4]);
console.log(set2); // Set {1, 2, 3, 4}
// 基本操作
set.add(1);
set.add(2);
set.add(1); // 重复值不会被添加
console.log(set); // Set {1, 2}
2. Map
Map 是一种键值对的集合,类似于对象,但键可以是任意类型(包括对象、函数等)。
// 创建 Map
const map = new Map();
// 通过数组创建
const map2 = new Map([
['name', 'Alice'],
['age', 25]
]);
// 基本操作
map.set('key1', 'value1');
map.set(123, 'number key');
map.set({ id: 1 }, 'object key');
console.log(map.get('key1')); // 'value1'
详细解答
1. Set 的详细用法
const set = new Set();
// 添加元素
set.add(1);
set.add(2);
set.add(3);
// 检查元素是否存在
console.log(set.has(1)); // true
console.log(set.has(4)); // false
// 删除元素
set.delete(2);
// 获取大小
console.log(set.size); // 2
// 清空
set.clear();
// 遍历
const colors = new Set(['red', 'green', 'blue']);
// for...of
for (const color of colors) {
console.log(color);
}
// forEach
colors.forEach((value, key) => {
console.log(key, value); // key 和 value 相同
});
// 转换为数组
const arr = [...colors];
const arr2 = Array.from(colors);
2. Map 的详细用法
const map = new Map();
// 设置键值对
map.set('name', 'Alice');
map.set('age', 25);
// 获取值
console.log(map.get('name')); // 'Alice'
// 检查键是否存在
console.log(map.has('name')); // true
// 删除键值对
map.delete('age');
// 获取大小
console.log(map.size);
// 清空
map.clear();
// 遍历
const userMap = new Map([
['name', 'Bob'],
['age', 30],
['city', 'Beijing']
]);
// for...of (返回 [key, value])
for (const [key, value] of userMap) {
console.log(key, value);
}
// forEach
userMap.forEach((value, key) => {
console.log(key, value);
});
// 获取所有键/值/条目
console.log([...userMap.keys()]);
console.log([...userMap.values()]);
console.log([...userMap.entries()]);
3. Set 和 Map 与普通对象、数组的对比
| 特性 | Set | Array | Map | Object |
|---|---|---|---|---|
| 数据类型 | 值的集合 | 有序列表 | 键值对 | 键值对 |
| 重复性 | 不重复 | 允许重复 | 键唯一 | 键唯一(字符串/Symbol) |
| 键类型 | 无(只有值) | 索引(数字) | 任意类型 | 字符串/Symbol |
| 顺序性 | 按插入顺序 | 按索引顺序 | 按插入顺序 | 不保证顺序 |
| 查找效率 | O(1) | O(n) | O(1) | O(1) |
| 适用场景 | 去重、集合运算 | 有序数据 | 复杂键的映射 | 简单数据结构 |
深入理解
1. Set 的去重原理
// Set 使用 SameValueZero 算法判断相等
const set = new Set();
set.add(1);
set.add('1');
set.add(1); // 重复,不会添加
// 特殊值
set.add(NaN);
set.add(NaN); // 不会添加,Set 认为 NaN 等于 NaN
set.add(+0);
set.add(-0); // 不会添加,+0 和 -0 相等
console.log(set); // Set {1, '1', NaN, 0}
// 对象的去重(引用比较)
const objSet = new Set();
objSet.add({ a: 1 });
objSet.add({ a: 1 }); // 会添加,因为是不同引用
const obj = { b: 2 };
objSet.add(obj);
objSet.add(obj); // 不会添加,同一引用
2. Map 的键比较
const map = new Map();
// 不同类型的键
map.set('1', 'string');
map.set(1, 'number');
map.set(true, 'boolean');
console.log(map.get('1')); // 'string'
console.log(map.get(1)); // 'number'
// 对象作为键
const keyObj = { id: 1 };
map.set(keyObj, 'object key');
console.log(map.get(keyObj)); // 'object key'
// 只有同一引用才能获取
console.log(map.get({ id: 1 })); // undefined
3. Set 的集合运算
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
// 并集
const union = new Set([...setA, ...setB]);
console.log(union); // Set {1, 2, 3, 4, 5, 6}
// 交集
const intersection = new Set([...setA].filter(x => setB.has(x)));
console.log(intersection); // Set {3, 4}
// 差集
const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log(difference); // Set {1, 2}
// 子集判断
const isSubset = (setA, setB) => {
return [...setA].every(x => setB.has(x));
};
console.log(isSubset(new Set([1, 2]), setA)); // true
最佳实践
1. Set 的应用场景
// 1. 数组去重
const numbers = [1, 2, 2, 3, 4, 4, 5];
const unique = [...new Set(numbers)];
console.log(unique); // [1, 2, 3, 4, 5]
// 2. 字符串去重
const str = 'hello world';
const uniqueChars = [...new Set(str)].join('');
console.log(uniqueChars); // 'helo wrd'
// 3. 检查重复
const hasDuplicates = (arr) => {
return new Set(arr).size !== arr.length;
};
// 4. 记录已访问元素
const visited = new Set();
function processNode(node) {
if (visited.has(node.id)) return;
visited.add(node.id);
// 处理节点...
}
// 5. 存储唯一标签
const tags = new Set();
tags.add('javascript');
tags.add('javascript'); // 忽略
tags.add('react');
2. Map 的应用场景
// 1. 缓存计算结果
const cache = new Map();
function fibonacci(n) {
if (cache.has(n)) return cache.get(n);
if (n <= 1) return n;
const result = fibonacci(n - 1) + fibonacci(n - 2);
cache.set(n, result);
return result;
}
// 2. 存储元数据(对象作为键)
const metadata = new Map();
const user = { name: 'Alice' };
metadata.set(user, { created: Date.now(), visits: 0 });
// 3. 计数器
const counter = new Map();
const words = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple'];
words.forEach(word => {
counter.set(word, (counter.get(word) || 0) + 1);
});
console.log(counter.get('apple')); // 3
// 4. 双向映射
const nameToId = new Map();
const idToName = new Map();
function addMapping(id, name) {
nameToId.set(name, id);
idToName.set(id, name);
}
// 5. 配置对象(支持任意键)
const config = new Map([
['apiUrl', 'https://api.example.com'],
[Symbol('timeout'), 5000],
[{ env: 'prod' }, 'production']
]);
3. WeakSet 和 WeakMap 简介
// WeakSet:只能存储对象,对象被回收时自动移除
const weakSet = new WeakSet();
let obj = { data: 'sensitive' };
weakSet.add(obj);
obj = null; // 对象被垃圾回收,weakSet 中自动移除
// WeakMap:键必须是对象,对象被回收时键值对自动移除
const weakMap = new WeakMap();
let key = { id: 1 };
weakMap.set(key, 'value');
key = null; // 键被垃圾回收,weakMap 中自动移除
// 适用场景:DOM 节点关联数据
const elementData = new WeakMap();
const button = document.querySelector('#btn');
elementData.set(button, { clickCount: 0 });
// 当 button 从 DOM 移除后,关联数据会被自动清理
面试要点
- Set 特点:无序、唯一、值不重复
- Map 特点:键值对、键可以是任意类型、按插入顺序排列
- 去重原理:使用 SameValueZero 算法,NaN 等于 NaN,+0 等于 -0
- 与 Object 的区别:Map 的键可以是任意类型,Object 的键只能是字符串或 Symbol
- WeakSet/WeakMap:弱引用,不阻止垃圾回收,适合 DOM 节点关联数据
常见面试题
// 面试题 1:如何实现数组去重?
const arr = [1, 2, 2, 3, 3, 3, 4];
// 方法1:Set
const unique1 = [...new Set(arr)];
// 方法2:filter
const unique2 = arr.filter((item, index) => arr.indexOf(item) === index);
// 面试题 2:Map 和 Object 的区别?
// - Map 的键可以是任意类型,Object 只能是字符串/Symbol
// - Map 保留插入顺序,Object 不保证
// - Map 有 size 属性,Object 需要手动计算
// - Map 更适合频繁增删的场景
// 面试题 3:如何实现两个数组的交集?
const intersection = (arr1, arr2) => {
const set2 = new Set(arr2);
return [...new Set(arr1)].filter(x => set2.has(x));
};
// 面试题 4:为什么 Map 比 Object 更适合做哈希表?
// 1. Map 的键类型更灵活
// 2. Map 的迭代顺序确定
// 3. Map 的性能在频繁增删时更好
// 4. Map 可以直接获取大小
// 面试题 5:WeakMap 的应用场景?
// 1. 私有属性实现
const privateData = new WeakMap();
class Person {
constructor(name) {
privateData.set(this, { name });
}
getName() {
return privateData.get(this).name;
}
}
// 2. DOM 节点数据缓存
const domCache = new WeakMap();
function getData(element) {
if (!domCache.has(element)) {
domCache.set(element, { clicks: 0 });
}
return domCache.get(element);
}