为什么需要 Map 和 Set
JavaScript 的 Object 虽然可以用作字典(key-value 存储),但它本质上不是为键值存储设计的——它的 key 只能是字符串或 Symbol,且有自己的原型链。Map 和 Set 是 ES6 引入的真正为键值存储和集合操作设计的数据结构。
理解它们与 Object/Array 的区别,以及 WeakMap/WeakSet 的弱引用机制,是掌握 JavaScript 高级数据结构的必经之路。
一、Map
1.1 Map vs Object
| 特性 | Map | Object |
|---|---|---|
| key 类型 | 任意类型 | 字符串或 Symbol |
| key 顺序 | 保持插入顺序 | 基本上保持(但有例外) |
| 大小 | Map.size | Object.keys(obj).length |
| 迭代 | 原生可迭代 | 需要 Object.entries() |
| 原型链 | 无(原生的干净对象) | 有(可能需要 hasOwnProperty) |
| 性能 | 优化用于键值存储 | 一般用途 |
// Object 作为字典的问题
const obj = {};
obj[{}] = 'value'; // {} 被转成字符串 "[object Object]"
obj[{toString: () => 'key'}] = 'value2'; // 覆盖
// Map 使用严格相等比较 key
const map = new Map();
const key1 = {};
const key2 = {};
map.set(key1, 'value');
map.set(key2, 'value2');
console.log(map.get(key1)); // 'value'
console.log(map.get(key2)); // 'value2'
1.2 Map 的基本操作
const map = new Map();
// 设置
map.set('name', 'Alice');
map.set(1, 'number key');
map.set({}, 'object key');
// 获取
console.log(map.get('name')); // 'Alice'
// 检查
console.log(map.has('name')); // true
// 删除
map.delete('name');
// 大小
console.log(map.size); // 3
// 清空
map.clear();
1.3 Map 的迭代
const map = new Map([
['name', 'Alice'],
['age', 30],
['city', 'NYC']
]);
// for...of
for (const [key, value] of map) {
console.log(`${key}: ${value}`);
}
// forEach
map.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
// 解构
[...map.keys()]; // ['name', 'age', 'city']
[...map.values()]; // ['Alice', 30, 'NYC']
[...map.entries()]; // [['name', 'Alice'], ['age', 30], ['city', 'NYC']]
二、Set
2.1 Set 的特性
Set 是值的集合,不允许重复:
const set = new Set();
set.add(1);
set.add(2);
set.add(1); // 重复,被忽略
console.log(set.size); // 2
console.log(set.has(1)); // true
set.delete(1);
console.log(set.has(1)); // false
2.2 Set 的去重机制
Set 使用 SameValueZero 算法判断相等,类似于 ===,但 NaN 等于自身:
const set = new Set();
set.add(NaN);
set.add(NaN); // 忽略,NaN === NaN 为 false,但 SameValueZero 认为相等
console.log(set.size); // 1
set.add({});
set.add({}); // 两个不同的空对象,不是重复
console.log(set.size); // 3
2.3 Set 的实用场景
// 数组去重
const arr = [1, 2, 2, 3, 3, 3];
const unique = [...new Set(arr)]; // [1, 2, 3]
// 记录访问
const visited = new Set();
function visit(url) {
if (visited.has(url)) {
console.log('Already visited');
return;
}
visited.add(url);
console.log('First visit');
}
// 交集、并集、差集
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
// 并集
const union = new Set([...setA, ...setB]);
// 交集
const intersection = new Set([...setA].filter(x => setB.has(x)));
// 差集 (A - B)
const difference = new Set([...setA].filter(x => !setB.has(x)));
三、WeakMap
3.1 弱引用的概念
普通 Map/Set 持有对象的引用,阻止垃圾回收:
const map = new Map();
const obj = {};
map.set(obj, 'value');
// 即使 obj 不再被其他代码使用
// map 仍然持有 obj 的引用,obj 不会被 GC 回收
obj = null; // map.obj 仍然存在
WeakMap 不阻止垃圾回收。当 WeakMap 的 key 对象没有任何其他引用时,该 key-value 对会被自动清理。
3.2 WeakMap 的特性
const wm = new WeakMap();
// key 必须是对象
wm.set('string', 'value'); // TypeError
wm.set({}, 'value'); // OK
// 不支持迭代
// [...wm] // TypeError
// wm.forEach // undefined
// 不支持 size
// wm.size // undefined
3.3 WeakMap 的应用
私有属性:
const privateData = new WeakMap();
class User {
constructor(name, password) {
privateData.set(this, { name, password });
}
getName() {
return privateData.get(this).name;
}
authenticate(password) {
return privateData.get(this).password === password;
}
}
// privateData 持有 User 实例的弱引用
// User 实例被 GC 后,privateData 中的对应条目自动清理
DOM 节点的元数据:
const metadata = new WeakMap();
metadata.set(element, {
lastClicked: Date.now(),
clickCount: 0
});
function handleClick(element) {
const data = metadata.get(element);
data.clickCount++;
data.lastClicked = Date.now();
}
四、WeakSet
4.1 WeakSet 的特性
WeakSet 与 Set 类似,但:
- 元素必须是对象
- 元素弱引用(无其他引用时可被 GC)
- 不可迭代,不支持 size
const ws = new WeakSet();
const obj1 = {};
const obj2 = {};
ws.add(obj1);
ws.add(obj2);
ws.add('string'); // TypeError
console.log(ws.has(obj1)); // true
ws.delete(obj1);
console.log(ws.has(obj1)); // false
4.2 WeakSet 的应用
对象标记:
const marked = new WeakSet();
// 标记已处理的对象
function processObject(obj) {
if (marked.has(obj)) {
console.log('Already processed');
return;
}
// 处理逻辑...
marked.add(obj);
}
保持对象活跃性检查:
// 验证对象是否还在使用
const activeObjects = new WeakSet();
function register(obj) {
activeObjects.add(obj);
}
function isActive(obj) {
return activeObjects.has(obj);
}
五、Map/Set 性能对比
5.1 插入、查找、删除性能
| 操作 | Map | Object |
|---|---|---|
| 插入 | O(1)* | O(1) |
| 查找 | O(1)* | O(1) |
| 删除 | O(1)* | O(1) |
*平均情况,取决于哈希函数质量
5.2 何时使用 Map vs Object
使用 Map:
- key 是非字符串类型(对象、函数等)
- 需要迭代或遍历
- 需要维护插入顺序
- key-value 操作频繁
使用 Object:
- key 是字符串或 Symbol
- 需要原型链
- 需要 JSON 序列化(直接)
- 需要方法(class 的原型方法)
六、面试高频考点
考点 1:Map 和 Object 的区别
Map 的 key 可以是任意类型,使用 SameValueZero 比较;Object 的 key 只能是字符串或 Symbol。Map 原生可迭代,有 size 属性。
考点 2:WeakMap 和 Map 的区别
WeakMap 的 key 是弱引用,当 key 对象没有其他引用时会被 GC 清理。WeakMap 不可迭代,不支持 size。
考点 3:Set 的去重原理
Set 使用 SameValueZero 算法,类似于 ===,但 NaN 被视为等于自身。对象引用比较的是引用相等(同一对象),而非值相等。