闭包通过作用域隔离与受控接口实现原始数据不可变性:内部状态不暴露,get()返回只读副本,update()强制校验,subscribe()传递冻结快照,杜绝引用泄露、校验缺失和多实例污染。

闭包本身不提供内存级不可变性,但能通过作用域隔离 + 显式控制接口,实现“原始数据不可变性”的强制约束效果:外部无法直接读写内部状态,所有变更必须经过校验逻辑,且返回值默认为只读副本。
用闭包锁住状态与规则,只暴露受控入口
核心是把状态变量(如 state)、合法变更路径(如 allowedTransitions)和订阅者列表(如 subs)全部声明在工厂函数内部,不挂载到返回对象上:
- 返回对象仅含 get()(返回结构化快照,非引用)、update(newVal)(带类型/范围/流转校验)、subscribe(fn) 和 unsubscribe(fn)
- 禁止返回
{ state }或state原始引用;get() 应返回JSON.parse(JSON.stringify(state))或Object.freeze({ ...state }) - update() 内部做深比较或 schema 校验,拒绝非法值、空对象、未定义字段等
订阅机制需绑定状态快照,而非实时引用
避免订阅者拿到后直接修改原始数据。每次 notify() 时,向回调传入的是当前状态的只读副本:
- 不执行
fn(currentState),而执行fn(Object.freeze({ ...currentState })) - 若状态含嵌套对象或数组,get() 和通知参数都应递归冻结或深拷贝(可用
structuredClone,兼容现代环境) - 订阅者若需持久化该快照,由其自行处理;中心不承担后续变异责任
防止意外泄露:三类典型破功场景及修复
即使用了闭包,以下操作仍会破坏“不可变性”约束:
-
返回可变对象引用:如
getData() { return internalList; }→ 改为getItems() { return [...internalList]; }或getItemsCount() { return internalList.length; } - 校验逻辑缺失:update 接收任意对象且直接赋值 → 必须校验字段名、类型、枚举值、必填项
-
未隔离多实例状态:用模块级变量代替闭包内变量 → 确保每次调用工厂函数都生成全新作用域,例如
const storeA = createStateCenter(initA); const storeB = createStateCenter(initB);
配合冻结与代理进一步加固(可选增强)
闭包提供逻辑隔离层,冻结(Object.freeze)和代理(Proxy)可叠加运行时防护:
- 初始化时对初始状态做
Object.freeze,阻止浅层篡改 - 在 update() 内部,对新值先
structuredClone再冻结,再替换旧状态 - 如需拦截属性访问/赋值异常,可在闭包内用
Proxy包裹状态对象,但注意性能开销;闭包已控入口,Proxy 更多用于调试提示而非必需

















