JavaScript闭包:模块模式与私有变量

小岛来信
小岛来信
发布时间:2026-09-24 | 8人浏览过
原创

闭包让函数在外层函数返回之后,仍能访问创建时所在作用域的变量。用工厂函数返回一组操作方法,就能把状态留在作用域内部。这里的“私有”指外部没有直接访问变量的接口,不等于安全隔离或加密。

用两个实例验证状态归属

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。

同一次创建共享状态,不同实例各自独立;外部只能通过返回的方法读写内部状态
外部只能通过返回的方法读写内部状态
同一实例共享状态,不同实例相互独立
a 连续递增两次得到 2,b 仍为 0;a.value 为 undefined,内部状态通过方法访问。

从返回的方法追踪变量属于谁

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 属性,复杂状态则需要按返回值结构继续增加针对性检查。

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表