先记住这个答案
做法是在类所在的模块里建一个不导出的 WeakMap,构造函数里执行 privates.set(this, { ... }),方法里用 privates.get(this) 取数据。外部拿不到 privates 这个变量,所以访问不到数据。用 WeakMap 而不是 Map,是因为它对键是弱引用,实例没人用了会被正常回收,私有数据跟着消失。它的缺点是写法啰嗦、每次访问多一次查表、调试时在实例上看不到这些数据;# 私有字段在语言层面解决了这些问题,是现在的首选。
WeakMap以实例为键,私有性来自模块作用域- 键是弱引用,实例回收后私有数据跟着回收
#字段是语言级私有,类外访问直接语法错误- 私有字段可用
#x in obj检查品牌
一个计数器的两种写法
同一个需求:计数器的 count 不允许外部直接改,只能通过 increment() 加。
WeakMap 版和 # 版对照JavaScriptconst privates = new WeakMap() // 模块内变量,不导出
class CounterA {
constructor() { privates.set(this, { count: 0 }) }
increment() { return ++privates.get(this).count }
}
class CounterB {
#count = 0
increment() { return ++this.#count }
}
const a = new CounterA()
const b = new CounterB()
a.increment()
b.increment()
console.log(a.count, Object.keys(a).length)
console.log(b.count, Object.keys(b).length)
console.log(a.increment(), b.increment())查看输出与解释
undefined 0
undefined 0
2 2两个实例对外都看不到 count,Object.keys 也是空的。区别在数据放哪:CounterA 的数据在实例外面的一张表里,实例只是查表用的钥匙;CounterB 的数据就挂在实例身上,只是名字带 #,类外面的代码没法按这个名字去拿。
为什么非得是 WeakMap
换成普通 Map 也能实现私有,但会出内存问题。Map 对键是强引用:只要 Map 还在,里面的实例就永远不会被回收,哪怕业务代码早就不用它了。一个频繁创建销毁的组件用这种写法,内存会一直涨。
WeakMap 的键是弱引用,它不会阻止键被垃圾回收。实例在别处没有引用了,回收器可以直接回收它,对应的条目也随之失效。代价是 WeakMap 不能遍历、没有 size,因为条目随时可能消失。对私有数据来说这正好合适,本来也不需要遍历。
另一个限制是键只能是对象(以及未注册的 Symbol),所以这个方案只适合以实例为单位存数据。
两种方案的实际差别
WeakMap 方案出现得早,ES2015 就能用,不需要编译也能在老环境运行,Babel 早期编译 # 字段时生成的代码也正是 WeakMap。它的私有边界是模块:同一个文件里的其他函数、其他类都能读写 privates,有时这反而方便,比如两个类需要共享内部状态。
# 私有字段的边界是类体。类外面写 obj.#count 直接是语法错误,代码都跑不起来;在类里对一个没有这个字段的对象访问 #count,会抛 TypeError,而 WeakMap.get 只会安静地返回 undefined。访问速度上,# 字段是引擎直接支持的属性访问,比查一次 WeakMap 快。在 Chrome DevTools 里还能直接展开看到 #count 的值,调试体验好很多。
容易答错的地方
- 以为
WeakMap方案做到了绝对私有 - 它的私有只靠作用域。如果有人不小心把
privates导出了,或者同模块的代码写错了,数据照样能被改。#字段才是语言保证的私有。 - 以为
WeakMap的值也是弱引用 - 弱的只有键。值是被强引用的,只是当键被回收后,这个条目整体失效,值才跟着可以被回收。如果值里反过来引用了键,也不会造成泄漏,这是
WeakMap专门处理过的情况。 - 以为
#字段能被Proxy透明代理 #字段的访问检查的是对象本身,Proxy包一层之后,方法里的this变成代理对象,访问this.#count会抛TypeError。WeakMap方案同样会查不到,因为代理对象不是原来那个键。
面试官还会怎么问?
子类能访问父类的 # 私有字段吗?
不能。# 字段只在声明它的那个类体内可见,子类里写 this.#count 是语法错误。子类想用,父类要提供方法或者改用约定的 _count 形式。WeakMap 方案如果父子类在同一个模块,倒是可以共享同一张表。
怎么判断一个对象是不是某个类真正创建的?
# 字段支持 #count in obj 这种写法,返回布尔值,不会报错,比 instanceof 可靠,因为 instanceof 只看原型链,改个原型就能骗过。WeakMap 方案对应的写法是 privates.has(obj)。
为什么不用 Symbol 做私有属性名?
Symbol 属性不会出现在 Object.keys 和 for...in 里,但 Object.getOwnPropertySymbols 能把它们全部列出来,拿到后就能随意读写。所以 Symbol 只能算「不容易撞名」,谈不上私有。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。