object.freeze仅冻结对象自身属性,不递归冻结嵌套对象;需手动实现深冻结,跳过原始值、函数、内置对象及已冻结对象,并用weakmap防循环引用,且须在模块加载前同步执行。

Object.freeze 只冻结对象自身属性,不递归处理嵌套对象。父对象被冻结后,子对象仍可自由修改——这不是 bug,而是设计使然。要真正切断篡改路径,得主动补上深冻结这一环。
识别哪些子对象需要冻结
只对纯对象(plain object)和数组递归冻结,跳过以下类型:
- 原始值(string、number、boolean、null、undefined)
- 函数(冻结无意义,且可能报错)
- Date、RegExp、Map、Set 等内置对象(配置中应避免出现)
- 已冻结的对象(用 Object.isFrozen 提前检查)
实现安全的深冻结函数
手动递归时注意顺序和覆盖范围:
- 先遍历所有自有属性名:用 Object.getOwnPropertyNames(obj) + Object.getOwnPropertySymbols(obj),确保 Symbol 键不遗漏
- 先冻结子对象,再冻结父对象,保证引用链完整锁定
- 加入 WeakMap 缓存已处理对象,防止循环引用导致栈溢出
在正确时机执行冻结
冻结必须发生在任何模块访问之前:
- Node.js:在入口文件(如 index.js)顶部同步加载配置,立即 deepFreeze,再 require 其他模块
- 浏览器 ESM:import 配置后立刻调用 deepFreeze,不能依赖导出语句里写 Object.freeze({})
- 避免模块缓存未冻结版本——比如某个模块提前 const cfg = import('./config'),之后再 freeze,其他模块拿到的仍是可变引用
额外加固手段
深冻结不是万能盾,还需配合其他防护:
- 冻结前调用 Object.setPrototypeOf(config, null),切断原型链,防通过 __proto__ 注入
- 用 ESLint 规则 no-param-reassign 和 no-const-assign 在开发阶段拦截误操作
- 关键字段可用 Object.getOwnPropertyDescriptors 检查 writable 和 configurable 是否全为 false











