先记住这个答案
WeakMap 的键过去只能是对象,ES2023 起也可以是非注册的 Symbol,也就是用 Symbol() 创建的那种。字符串、数字这些原始值不能当键,Symbol.for() 创建的注册 Symbol 也不行,因为它们永远不会被回收。WeakMap 对键是弱引用:它存着这个键,但不算作「还有人在用」。键对象在程序别处没有引用了,垃圾回收器就可以回收它,对应的条目也一起消失。因为条目随时可能消失,WeakMap 没有 size,也不能遍历,只能拿着已知的键去 get、has、set、delete。
- 键只能是对象或非注册 Symbol
- 对键是弱引用,不会阻止键对象被回收
- 没有
size,也不能遍历,只能按已知的键查 - 适合给对象挂附加数据,不适合做需要淘汰策略的缓存
键的限制:对象按引用,原始值不行
下面给一个任务对象挂上附加数据,再试试用字段相同的新对象查,以及两种 Symbol 当键的情况。
const metadata = new WeakMap()
const task = { id: 1 }
metadata.set(task, { checked: true })
console.log(metadata.get(task).checked)
console.log(metadata.has({ id: 1 })) // 新对象,不是同一个键
const token = Symbol('local')
metadata.set(token, 'allowed') // 非注册 Symbol 可以当键
console.log(metadata.get(token))
try {
metadata.set(Symbol.for('shared'), 'invalid') // 注册 Symbol 不行
} catch (error) {
console.log(error.name)
}查看输出与解释
true
false
allowed
TypeErrorhas({ id: 1 }) 是 false,因为键按引用比较,字段一样的新对象是另一个键。Symbol('local') 每次创建都是唯一的,没有引用后可以被回收,所以能当键。Symbol.for('shared') 存在全局注册表里,任何地方都能再取到它,永远不会被回收,放进 WeakMap 就失去了弱引用的意义,所以直接抛 TypeError。
为什么需要弱引用
假设要给页面上的 DOM 节点记录一些状态,比如某个节点被点击了几次。用 Map 存的话,节点从页面上移除后,Map 里还引用着它,节点和它的数据都释放不了,这就是内存泄漏。换成 WeakMap,节点在别处没有引用后,条目会自动被回收,不用手动清理。
- 适合和不适合的场景
- 适合的场景有两类。一是给别人的对象附加数据,又不想改对象本身,比如缓存某个对象的计算结果,对象没了缓存也就没了。二是存类实例的私有数据,在
#私有字段出现之前,这是常见的写法。 不适合的场景是需要控制容量的缓存。WeakMap不能遍历,没法实现「最多存 100 条,超了淘汰最久没用的」这种策略,这时要用Map自己管理。
容易答错的地方
- 还在说
WeakMap的键只能是对象 - 这是 ES2023 之前的说法。现在非注册 Symbol 也可以当键,注册 Symbol 仍然不行。回答时可以补一句:老版本的运行环境可能还不支持 Symbol 键,用之前要确认。
- 以为
WeakMap会弱引用值 - 弱引用的只是键。值由条目强引用着,键活着值就一直在;键被回收后,值如果没有别的地方引用,也会被回收。值里反过来引用了键,也不会导致键回收不掉,
WeakMap专门处理了这种情况。 - 把对象设为
null就认为内存马上释放了 obj = null只是去掉了一个引用,别处可能还有引用,比如闭包、事件监听、数组里。即使没有任何引用了,什么时候回收也由引擎决定。
面试官还会怎么问?
WeakMap 为什么不能遍历,也没有 size?
条目什么时候消失取决于垃圾回收,而回收时机是不确定的。如果能遍历或计数,同一段代码在不同时刻会得到不同的结果,程序行为就依赖于回收时机了,所以规范故意不提供这些能力。
值可以是字符串或数字吗?
可以。限制只针对键,值可以是任何类型。
WeakMap 能替代组件卸载时的清理逻辑吗?
不能。它只负责让数据跟着键一起被回收,不会帮你调用取消订阅、清除定时器、关闭连接这些操作。组件卸载时该做的清理还是要显式写,比如在 useEffect 的清理函数里。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。