Map、Set 和 WeakMap、WeakSet 有什么区别?
一句话回答
Map 是键值对集合,键可以是任意类型,并保持插入顺序;Set 是不重复值的集合,常用来去重。WeakMap 的键、WeakSet 的成员必须是对象(ES2023 起也可以是未注册的 Symbol),并且是弱引用,不会阻止垃圾回收,也因此不能遍历、没有 size。WeakMap 适合给对象关联数据、按对象缓存结果,对象被回收后,关联的数据会自动释放。
详细解析
Map 和普通对象
| Map | 普通对象 | |
|---|---|---|
| 键的类型 | 任意类型,包括对象和函数 | 字符串或 Symbol,其他类型会被转成字符串 |
| 顺序 | 按插入顺序 | 整数键按升序排在最前面,其余按插入顺序 |
| 大小 | size 属性 |
要用 Object.keys(obj).length 计算 |
| 遍历 | 本身可迭代,直接 for...of |
借助 Object.keys、Object.entries |
| 和原型属性冲突 | 不会 | 可能,比如 toString;可以用 Object.create(null) 避免 |
| 频繁增删 | 通常性能更好 | 没有针对这种场景优化 |
| JSON 序列化 | 不能直接序列化 | 可以 |
const map = new Map()
map.set(1, 'number').set('1', 'string')
console.log(map.size) // 2:数字 1 和字符串 '1' 是不同的键
const obj = {}
obj[1] = 'number'
obj['1'] = 'string'
console.log(Object.keys(obj).length) // 1:数字键被转成了字符串 '1'
Set
Set 存储不重复的值,最常见的用法是数组去重:
const unique = [...new Set([3, 1, 3, 2, 1])] // [3, 1, 2]
const set = new Set([NaN, NaN, {}, {}])
console.log(set.size) // 3:NaN 只算一个,两个 {} 是不同的引用
Map 的键和 Set 的值都用 SameValueZero 算法判断是否相同:和 === 的区别是认为 NaN 等于 NaN;和 Object.is 的区别是认为 +0 等于 -0。
WeakMap 和 WeakSet
| Map / Set | WeakMap / WeakSet | |
|---|---|---|
| 键(成员)的类型 | 任意值 | 只能是对象;ES2023 起也可以是未注册的 Symbol(不是用 Symbol.for 创建的) |
| 引用方式 | 强引用,会阻止回收 | 弱引用,不阻止回收 |
遍历、size、clear |
有 | 都没有 |
| 方法 | 完整 | WeakMap 只有 get、set、has、delete;WeakSet 只有 add、has、delete |
弱引用的意思是:键对象在其他地方都不再被引用后,可以被垃圾回收,它在 WeakMap 中的条目也随之消失。值是强引用,但只在键存活期间被持有。
应用场景
- 给 DOM 节点或对象关联额外数据:节点被移除并回收后,数据自动释放,不用手动删除
- 存放对象的私有数据:以实例为键保存私有状态,拿不到这个 WeakMap 就访问不到
- 按对象缓存计算结果:对象不再使用时,缓存也跟着释放
- 深拷贝时记录已拷贝过的对象:用来处理循环引用,见深拷贝
- Vue 3 的响应式系统:用 WeakMap 以原始对象为键存储依赖关系,见 Vue 3 响应式原理
const cache = new WeakMap()
function getStats(data) {
if (!cache.has(data)) {
cache.set(data, heavyCompute(data)) // 同一个对象只计算一次
}
return cache.get(data)
}
// data 对象不再被使用后,缓存的结果会随它一起被回收
WeakRef 和 FinalizationRegistry
了解即可。WeakRef 持有一个对象的弱引用,deref() 取回对象,对象已被回收则返回 undefined;FinalizationRegistry 可以在对象被回收后执行一个回调。对象什么时候被回收、回调会不会执行都没有保证,不要让业务逻辑依赖它们。
面试官可能追问
WeakMap 为什么不能遍历,也没有 size?
键随时可能被回收,而回收什么时候发生由引擎决定,无法预测。如果允许遍历或读取 size,结果就取决于垃圾回收有没有发生,同样的代码可能得到不同的结果。所以规范不提供这些操作,只能用手里已有的键去查。
用对象作为 Map 的键要注意什么?
键按引用比较,必须是同一个对象才能取到值:
const map = new Map()
map.set({ id: 1 }, 'a')
map.get({ id: 1 }) // undefined:两个字面量是不同的对象
const key = { id: 1 }
map.set(key, 'b')
map.get(key) // 'b'
另外,Map 会一直强引用这个键对象,即使别处已经不用了,只要没从 Map 中删除,它就不会被回收。如果只是想给对象关联数据,用 WeakMap 更合适。
Map 和普通对象怎么选?
- 结构固定、键是已知的字符串、需要 JSON 序列化(如配置、接口数据):用对象
- 键在运行时动态增删,或者键不是字符串,或者需要按插入顺序遍历、知道元素个数:用 Map
- 以对象为键关联数据,又不想影响对象被回收:用 WeakMap
易错点
- WeakMap 只对键是弱引用,值是强引用
- Set 对对象按引用去重,内容相同的两个对象不会被去掉
- 不要用
map[key] = value给 Map 赋值,那样设置的是 Map 对象自身的属性,get取不到,size也不变
AI 模拟面试官
用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮
这道题你掌握了吗?
选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。
学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。