一、核心概念与区别总览
| 特性 | Set | Map | WeakSet | WeakMap |
|---|---|---|---|---|
| 本质 | 值的集合(唯一) | 键值对的集合 | 对象的弱引用集合 | 对象的弱引用键值对 |
| 键/值类型 | 任意类型 | 任意类型 | 只能是对象 | 键必须是对象,值任意 |
| 重复性 | 不可重复 | 键不可重复 | 不可重复 | 键不可重复 |
| 引用类型 | 强引用 (阻止GC) | 强引用 (阻止GC) | 弱引用 (不阻止GC) | 弱引用 (不阻止GC) |
| 可遍历性 | 支持 (for...of, keys等) |
支持 (for...of, keys等) |
不可遍历 | 不可遍历 |
| size 属性 | 有 | 有 | 无 | 无 |
| 主要用途 | 去重、集合运算 | 字典、缓存、关联数据 | 临时对象标记、私有属性 | 缓存、DOM节点关联数据 |
二、详细解析与常用 API
1. Set (集合)
核心特点:成员唯一,无序(但按插入顺序遍历)。
常用 API:
javascript
const mySet = new Set([1, 2, 2, 3]); // Set(3) {1, 2, 3}
// 增
mySet.add(4);
// 删
mySet.delete(2);
// 查
mySet.has(3); // true
mySet.size; // 3 (注意是属性不是方法)
// 遍历
mySet.forEach((value) => console.log(value));
for (let item of mySet) { console.log(item); }
// 转数组 (面试常考:数组去重)
const arr = [...mySet];
// 或 Array.from(mySet)
面试考点 & 实战:
-
数组去重 :
[...new Set(array)]是最简洁的去重方式。 -
集合运算 :利用 Set 实现交集、并集、差集。
javascriptconst a = new Set([1, 2, 3]); const b = new Set([3, 4, 5]); // 并集 const union = new Set([...a, ...b]); // 交集 const intersection = new Set([...a].filter(x => b.has(x))); // 差集 (a中有但b中没有) const difference = new Set([...a].filter(x => !b.has(x))); -
NaN 的处理 :Set 中
NaN被视为相等,只存一个。const s = new Set([NaN, NaN]); // size: 1
2. Map (字典)
核心特点 :键值对,键可以是任意类型(包括对象、函数),保留插入顺序。
常用 API:
javascript
const myMap = new Map();
const objKey = { id: 1 };
// 增/改
myMap.set('key1', 'value1');
myMap.set(objKey, 'object value'); // 对象作键
// 查
myMap.get('key1');
myMap.get(objKey);
myMap.has('key1');
myMap.size;
// 删
myMap.delete('key1');
myMap.clear();
// 遍历 (比对象更灵活)
for (let [key, value] of myMap) { ... }
myMap.forEach((value, key) => { ... });
面试考点 & 实战:
- Map vs Object :
- 键类型:Object 的键只能是字符串或 Symbol;Map 可以是任意类型。
- 顺序:Map 保证插入顺序;Object (ES6+) 虽也有顺序但主要设计为哈希表。
- 大小 :Map 有
size属性;Object 需手动计算。 - 性能:频繁增删键值对时,Map 通常优于 Object。
- 序列化 :Object 原生支持 JSON;Map 需转换 (
JSON.stringify([...map]))。
- 应用场景 :
- 需要非字符串键(如用 DOM 节点做键存储相关数据)。
- 需要频繁增删改查的大型数据集。
- 维护一个有序的键值对列表。
3. WeakSet & WeakMap (弱引用)
核心特点 :弱引用。如果对象没有其他引用指向它,垃圾回收机制 (GC) 会自动回收该对象,集合中的对应项也会自动消失。
关键限制:
- 只能存对象(不能存原始类型)。
- 不可遍历 (没有
keys(),values(),entries(),forEach)。 - 没有 size 属性(因为元素可能随时被 GC 回收,大小不确定)。
面试考点 & 实战:
-
防止内存泄漏 :这是最大的考点。
- 场景 :给 DOM 元素绑定临时数据。如果使用
Map,即使 DOM 元素从页面移除,只要 Map 还持有引用,该元素就无法被 GC,导致内存泄漏。使用WeakMap,当 DOM 元素被移除且无其他引用时,自动从 WeakMap 中清除。
- 场景 :给 DOM 元素绑定临时数据。如果使用
-
私有属性模拟 :
javascriptconst _privateData = new WeakMap(); class MyClass { constructor() { _privateData.set(this, { secret: '123' }); } getSecret() { return _privateData.get(this).secret; } } // 外部无法直接访问 _privateData 中的内容,且实例销毁后自动清理 -
缓存优化:用于缓存计算结果,当原对象被回收时,缓存也自动失效,避免缓存无限增长。
三、WeakMap、WeakSet使用场景
在日常的业务逻辑开发(如写页面交互、处理数据列表)中,确实很少直接显式地 new WeakMap() 或 new WeakSet()。
大多数时候,我们直接用 Map、Object 或者数组就能解决问题。
但是,"没用到"不代表"不重要" 。WeakMap 和 WeakSet 通常隐藏在底层库、框架源码、性能优化方案 以及防止内存泄漏的架构设计中。
以下是它们的具体用途:
1. 核心场景:防止内存泄漏(最经典用途)
这是 WeakMap 存在的最大意义。当你需要将数据与某个生命周期不确定 的对象(如 DOM 节点、第三方库生成的对象)绑定时,必须用 WeakMap。
场景描述:
假设你正在开发一个复杂的 Dashboard,页面上有成千上万个图表组件。你需要给每个 DOM 节点绑定一些私有数据(比如配置项、状态缓存)。
-
如果用
Map/ 全局对象:javascriptconst nodeDataMap = new Map(); function initChart(domNode) { const data = { config: {...}, state: 'active' }; nodeDataMap.set(domNode, data); // 强引用 } function destroyChart(domNode) { // ⚠️ 危险点:如果你忘记调用 delete,或者逻辑复杂漏删了 // nodeDataMap.delete(domNode); // 即使 domNode 从页面移除了(用户关闭了标签页), // 只要 nodeDataMap 还活着,domNode 就永远无法被 GC 回收! // 结果:内存泄漏,页面越来越卡,最终崩溃。 } -
如果用
WeakMap:javascriptconst nodeDataMap = new WeakMap(); // 弱引用 function initChart(domNode) { const data = { config: {...}, state: 'active' }; nodeDataMap.set(domNode, data); // 这里只是"暂时"关联。 } function destroyChart(domNode) { // ✅ 安全:不需要手动 delete! // 当 domNode 从 DOM 树移除,且代码中没有其他变量引用它时, // 浏览器 GC 会自动回收 domNode。 // 一旦 domNode 被回收,WeakMap 中对应的 entry 也会自动消失。 // 内存自动释放,零泄漏风险。 }
实际案例:
- DOM 库封装 :很多 UI 库(如早期的 jQuery 插件,或现在的自定义指令)内部用
WeakMap存储实例数据。 - 事件监听器管理:存储元素对应的事件处理函数映射。
2. 核心场景:实现"私有属性" (Private Properties)
在 ES6 class 的 #privateField 语法出现之前,WeakMap 是实现真正私有属性的唯一标准方案(比闭包更灵活,比 _variable 命名约定更安全)。
即使现在有了 # 语法,某些动态场景或 Babel 转译后的代码依然依赖 WeakMap。
代码示例:
javascript
const _secretData = new WeakMap();
class User {
constructor(name, token) {
this.name = name;
// 将敏感数据存储在 WeakMap 中,key 是当前实例(this)
_secretData.set(this, { token: token, role: 'admin' });
}
getToken() {
return _secretData.get(this).token;
}
}
const u = new User('Alice', 'xyz-123');
console.log(u.token); // undefined (外部无法访问)
console.log(u._secretData); // undefined (根本挂不在实例上)
// 只有类内部通过 _secretData.get(this) 才能拿到
优势:
- 不可枚举 :
Object.keys(u)看不到这些数据。 - 自动清理 :当
u被销毁,敏感数据也随之消失,不会残留在内存中。
3. 核心场景:高性能缓存 (Memoization)
当你需要缓存函数的计算结果,但输入参数是对象 时,WeakMap 是完美的缓存容器。
痛点:
如果你用普通 Map 做缓存:
javascript
const cache = new Map();
function heavyCalc(obj) {
if (cache.has(obj)) return cache.get(obj);
const res = doSomething(obj);
cache.set(obj, res);
return res;
}
// 问题:如果 obj 在业务中不再使用,但 cache 里还存着,导致 obj 无法回收。
// 随着时间推移,cache 会无限膨胀,吃掉所有内存。
解决方案:
javascript
const cache = new WeakMap(); // 弱引用缓存
function heavyCalc(obj) {
if (cache.has(obj)) return cache.get(obj);
const res = doSomething(obj);
cache.set(obj, res);
return res;
}
// 优势:一旦外部的 obj 被丢弃,缓存自动失效并释放内存。
// 实现了"智能缓存",无需手动管理缓存过期策略。
实际案例:
- CSS-in-JS 库 (如 styled-components, emotion):它们需要根据样式对象生成唯一的 class 名,内部大量使用
WeakMap来缓存映射关系。 - 响应式框架 (如 Vue 3, MobX):在建立对象属性与副作用函数(Effect)的依赖关系时,常用
WeakMap来存储依赖收集器,确保组件销毁后依赖关系自动断开。
4. 核心场景:标记对象状态 (WeakSet)
WeakSet 常用于标记某个对象是否处于某种临时状态,而不需要关心具体的值。
场景:防止重复处理
javascript
const processedNodes = new WeakSet();
function processNode(node) {
if (processedNodes.has(node)) {
return; // 已经处理过,跳过
}
// 执行耗时操作...
doHeavyWork(node);
processedNodes.add(node); // 标记为已处理
}
// 当 node 被移除出 DOM,它会自动从 processedNodes 中消失。
// 下次如果创建了一个全新的 node 对象(即使内容一样),也不会冲突。
总结:什么时候用?什么时候不用?
| 特性 | Map / Set | WeakMap / WeakSet |
|---|---|---|
| 引用强度 | 强引用 (防止 GC) | 弱引用 (允许 GC) |
| 可遍历性 | ✅ 可遍历 (keys, values, forEach) |
❌ 不可遍历 (黑盒) |
| Key 类型 | 任意类型 | 必须是对象 |
| 适用场景 | 需要列出所有项、需要持久化缓存、配置表 | 私有数据、框架元数据、防循环引用、DOM 关联数据 |
| 内存风险 | 容易泄漏 (忘了 delete) | 零泄漏 (自动清理) |
四、面试高频问题汇总 (Q&A)
Q1: Map 和 Object 有什么区别?什么时候用 Map?
- 回答要点 :
- 键的类型:Map 支持任意类型(对象、函数等),Object 仅限 String/Symbol。
- 顺序:Map 严格保持插入顺序。
- Size :Map 内置
size,Object 需手动统计。 - 性能:在频繁添加/删除键的场景下,Map 性能通常更好。
- 原型干扰 :Object 继承自原型链,可能有默认键(如
toString),Map 是纯净的。
- 结论:当键不是字符串、需要频繁增删、或需要保持顺序时,优先用 Map。纯数据配置可用 Object。
Q2: Set 如何实现数组去重?原理是什么?
- 回答 :
[...new Set(arr)]。 - 原理 :Set 内部通过类似"Same-value-zero"算法判断相等性(类似于
===,但NaN === NaN为 false,而 Set 认为NaN等于NaN)。添加时若已存在则忽略。
Q3: WeakMap 的键为什么必须是对象?
- 回答 :因为弱引用的机制是针对内存地址的。原始类型(string, number)是按值存储的,不存在"引用"概念,也没有所谓的"其他地方是否还有引用"的问题,所以无法实现弱引用逻辑。只有对象才有引用计数的概念。
Q4: 既然 WeakMap 这么好,为什么不全部用它?
-
回答 :因为它有局限性:
- 不可遍历:无法获取所有键或值,无法转换为 JSON。
- 不可预测的大小:无法知道当前有多少元素。
- 键必须是对象。
- 如果需要遍历、序列化或检查大小,必须用
Map。
Q5: Map 的键如果是对象,怎么判断是否相同?
-
回答 :Map 比较对象键时,比较的是内存地址(引用) ,而不是对象的内容。
javascriptconst map = new Map(); const a = { id: 1 }; const b = { id: 1 }; map.set(a, 'A'); map.get(b); // undefined (因为 a !== b,地址不同) map.get(a); // 'A'