防止配置被意外修改需冻结+封装+导入时机控制三者配合,必须在配置模块内部完成深度冻结后导出,且深冻结要递归处理嵌套对象、跳过不可冻结类型并防循环引用。

深拷贝本身不防修改,它只是复制数据;真正防止子模块意外修改公共配置,靠的是冻结 + 封装 + 导入时机控制三者配合。单纯用 structuredClone 或 JSON.parse(JSON.stringify()) 生成副本,若子模块拿到后直接改,照样破坏配置一致性。
冻结必须在导出前完成
模块导出的配置对象,必须在任何其他模块 import 之前就完成深度冻结。否则子模块 import 到的是原始引用,后续无论你怎么 freeze,都晚了。
- 错误做法:先导出未冻结对象,再在主入口调用 deepFreeze
- 正确做法:在配置模块内部定义、冻结、再导出 —— 冻结和导出是原子操作
- 示例:config.js
const RAW_CONFIG = { api: { baseUrl: 'https://a.com', timeout: 3000 }, features: { auth: true } };
export const APP_CONFIG = deepFreeze(RAW_CONFIG);
深冻结要处理嵌套与循环引用
业务配置常含多层对象或数组,浅冻结(Object.freeze)对 api.timeout 没用,对 api.headers 也没用。必须递归冻结每一层 plain object 和 array,同时跳过 Date、RegExp、函数等不可冻结类型,并用 WeakMap 防止循环引用死循环。
- 关键点:先冻结子节点,再冻结父节点(自底向上)
- 常见疏漏:忽略 Symbol 键、忽略 null/undefined 属性、未检查 Object.isFrozen 提前终止
- 建议直接复用经生产验证的 deepFreeze 实现,而非手写简易版
模块封装要切断原始引用泄漏
即使冻结了,如果配置模块还导出了构造函数、工厂方法、或未冻结的中间对象,子模块仍可能绕过防护拿到可变引用。
- 只导出最终冻结后的常量,不导出 RAW_CONFIG、createConfig()、或 configBuilder
- 避免在配置模块中混入类实例或带状态的对象(它们无法被安全冻结)
- 推荐用默认导出 + 命名空间隔离,例如:export default Object.freeze({...})
配合 ESLint 与 TypeScript 提前拦截
运行时冻结是最后一道防线,开发阶段就要卡住误操作。
- 启用 ESLint 规则:no-param-reassign(防函数内改配置)、no-const-assign(防重赋值)、no-mutation(插件级防护)
- TypeScript 中用 as const 推导字面量类型,配合 readonly 修饰符约束取值范围
- CI 流程中加入冻结校验脚本:import 后立即检查 Object.isFrozen,不通过则报错
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











