JavaScript闭包:模块模式与私有变量
闭包让函数在外层函数返回之后,仍能访问创建时所在作用域的变量。用工厂函数返回一组操作方法,就能把状态留在作用域内部。这里的“私有”指外部没有直接访问变量的接口,不等于安全隔离或加密。
用两个实例验证状态归属
const assert = require('node:assert/strict');
function createCounter() {
let value = 0;
return {
inc() { value += 1; return value; },
read() { return value; }
};
}
const a = createCounter();
const b = createCounter();
a.inc(); a.inc();
assert.equal(a.read(), 2);
assert.equal(b.read(), 0);
assert.equal(a.value, undefined);
console.log(a.read(), b.read(), a.value);
保存为 counter.cjs 运行,预期输出 2 0 undefined。a 的两个方法访问同一个 value;b 的方法访问另一次调用创建的 value。


从返回的方法追踪变量属于谁
createCounter 执行时创建 value,并返回 inc 与 read 两个方法。外层调用结束后,返回的方法仍然能够访问这次调用创建的变量,所以后续 inc 可以继续累加,read 也能读取累加后的值。变量不需要变成全局变量,才能在多次方法调用之间保留状态。
同一次工厂调用返回的两个方法共享同一份 value,而重新调用工厂会获得新的状态。因此 a.inc 不会把 b 的计数一起增加。排查状态相互影响时,先检查是不是意外复用了同一个工厂返回值,再检查是否把状态移到了工厂外部。
闭包保存的是访问关系,不是固定快照
inc 改变 value 之后,read 能看到新值。若把闭包理解为“把创建时的数值复制一份”,就无法解释这种共享变化。实际设计时,应明确哪些方法可以修改状态,避免从接口返回可随意修改的内部对象。
| 做法 | 结果 |
|---|---|
| 返回 read()/inc() 方法 | 外部通过明确接口操作状态 |
| 直接返回内部数组 | 外部可能绕过方法修改内容 |
| 每次调用工厂函数 | 创建独立的状态作用域 |
私有变量与公开对象属性不是一回事
示例中的 a.value 为 undefined,因为返回对象只公开 inc 和 read,没有公开 value 属性。给 a 额外添加一个 value 属性,并不会把它自动连接到闭包里的同名变量;对象属性访问与词法作用域变量访问是两条不同路径。
封装的目标是让调用者通过明确的方法操作内部状态。例如只提供递增和读取,就能限制接口层面允许的行为。但这不是加密机制,也不能用来承诺对不可信执行环境的安全隔离。应根据需求区分代码组织、访问接口和安全边界。
不要用闭包掩盖生命周期问题
只要仍有可达的回调引用闭包,它关联的必要状态就可能继续存活。事件订阅、定时器和缓存应有明确的释放时机。模块模式适合封装少量状态;大型对象模型也可以考虑类和私有字段,依据接口清晰度选择,而不是把闭包当作唯一方案。
接口返回引用时仍可能泄漏可变状态
如果内部保存数组,而 read 直接返回这个数组,调用方拿到的仍然是可变引用。它可以绕过你定义的修改方法直接 push 或删除元素。若不希望发生这种情况,可以返回适合当前数据结构的快照,或只暴露必要的查询方法;涉及嵌套对象时,还要明确快照复制的深度。
在工程中,闭包常随事件处理器或定时器一起存活。组件结束使用后,如果监听没有解除,回调仍可能持有它需要的状态。设计接口时应一并考虑订阅的取消和定时器的清理,而不是只关注变量是否“藏起来”。
验收封装至少检查三个方面:同实例修改能被读取,不同实例互不影响,公开接口不会无意泄漏内部可变对象。本例验证了前两点及没有直接 value 属性,复杂状态则需要按返回值结构继续增加针对性检查。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









