先记住这个答案
做法是写一个函数,把要保护的变量声明在函数里,然后返回一个对象,对象上只放几个能读写这个变量的方法。函数执行完之后,因为返回的方法还引用着这个变量,它不会被回收,这就是闭包。外面的代码拿到的只是方法,没有任何办法直接读或改这个变量,只能按方法规定的方式操作。常见的写法有两种:可以多次调用、每次产生一个新实例的工厂函数,和只执行一次、得到一个单例的模块模式(用 IIFE 包起来)。
- 变量放在函数里,只把操作它的方法返回出去
- 每调用一次工厂函数,就有一份独立的私有变量
- 下划线开头的属性只是约定,不是真私有
- 用
class时优先考虑#私有字段
工厂函数:每次调用一份新状态
最常用的写法,一个计数器就能说明所有要点。
function createCounter(initial = 0) {
let count = initial // 私有变量,不挂到返回的对象上
return {
increment() {
count += 1
return count
},
value() {
return count
}
}
}
const a = createCounter()
const b = createCounter(10)
a.increment()
a.increment()
console.log(a.value(), b.value()) // 两个实例互不影响
console.log(a.count) // 对象上没有 count
a.count = 100 // 只是给对象加了个新属性
console.log(a.value())查看输出与解释
2 10
undefined
2a 和 b 是两次调用得到的,每次调用都新建了一个作用域,里面各有一个 count,所以 a 加到 2,b 还是 10。
最后两行很关键:a.count = 100 没有报错,但它只是在返回的对象上新加了一个叫 count 的属性,和闭包里的那个 count 毫无关系,a.value() 还是 2。私有变量根本不是对象的属性,所以从对象上既读不到,也改不了。
模块模式:只需要一份时用 IIFE
如果整个应用只需要一份状态,比如一个全局配置、一个缓存,就不用工厂函数,直接用 IIFE 执行一次,把返回的对象存起来。写法是 const store = (function () { let data = {}; return { get(k) {...}, set(k, v) {...} } })()。原理和工厂函数一样,只是函数只调用一次。
在 ES 模块里,这件事更简单:模块本身就是一个独立作用域,模块顶层声明的变量只要不 export,外部就访问不到,效果和模块模式一样,不需要再包 IIFE。
和其它「私有」写法对比
JavaScript 里说「私有」,面试官一般想听你把下面几种写法比较一下。
- 下划线约定、
#私有字段、WeakMap - 下划线开头的属性,比如
this._count,只是团队约定,外面照样能读能改,没有任何保护。class里的#count是语言层面的私有字段,类外面写obj.#count直接是语法错误。它的好处是方法定义在原型上,所有实例共用,不像闭包那样每个实例都要创建一套新方法。写class时优先用它。WeakMap是以前没有#时的方案,用实例作为 key 存私有数据,实例被回收后数据也自动释放。现在有了#私有字段,这种写法用得少了。
容易答错的地方
- 以为
Object.freeze能保护闭包里的变量 Object.freeze冻结的是返回的对象,防止别人改掉或删掉上面的方法。闭包里的变量不在对象上,它本来就改不到,方法内部也照样能改它。两者保护的是不同的东西。- 以为闭包私有变量没有代价
- 每调用一次工厂函数,返回的方法都会重新创建一份。创建成千上万个实例时,内存会比把方法放在原型上的
class多。另外,闭包里如果引用了大对象,只要方法还活着,大对象就回收不了。
面试官还会怎么问?
返回的方法解构出来单独用,还能正常工作吗?
能。const { increment } = createCounter() 之后直接调用 increment() 没问题,因为方法里用的是闭包变量 count,没有用 this。这也是闭包写法比 class 方便的地方,class 的方法解构出来 this 就丢了。
多个实例想共享一个私有变量怎么办?
把变量放到工厂函数外面一层,比如模块顶层或者再包一层 IIFE,所有实例的方法都引用同一个变量。比如记录一共创建了多少个实例,就可以这么做。
React 的 useState 和这里的闭包有关系吗?
有。组件每次渲染都是一次函数调用,事件处理函数通过闭包读到的是那一次渲染时的 state。定时器或异步回调里读到旧值的「闭包陷阱」,就是回调记住的是旧那次渲染的变量。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。