object.freeze仅浅冻结,无法保护嵌套对象;需用deepfreeze递归冻结所有层级,且须在模块加载前完成,配合const声明与模块封装才能真正保障配置不可变。

Object.freeze 为什么不能直接冻结嵌套对象
直接对包含对象或数组的配置调用 Object.freeze,只能冻结顶层属性的可配置性与可写性,内部嵌套结构依然能被修改。比如 config.api.timeout 是个数字,它被锁住了;但若 config.features 是个对象,你仍可以改它的 enabled 字段——这会让“全局常量”的承诺失效。
- 只冻结一层:默认浅冻结,
Object.freeze({ a: { b: 1 } })后,config.a.b = 2仍会成功 - 常见误判场景:从 JSON 文件读取配置后直接
Object.freeze(config),以为万事大吉 - 真正安全的做法是递归冻结所有对象/数组值,或确保原始数据本身不含可变引用
如何安全地冻结含嵌套结构的配置对象
必须手动遍历并冻结每一层对象和数组。简单可靠的实现是写一个 deepFreeze 工具函数,它跳过原始值(string/number/boolean/null/undefined),对 object 和 array 类型递归调用 Object.freeze,并在冻结前检查是否已冻结(避免重复或循环引用报错)。
function deepFreeze(obj) {
if (obj === null || typeof obj !== 'object') return obj;
if (Object.isFrozen(obj)) return obj;
// 先冻结自身
Object.freeze(obj);
// 再冻结所有可枚举自有属性的值
Object.getOwnPropertyNames(obj).forEach(prop => {
if (obj[prop] !== null && typeof obj[prop] === 'object') {
deepFreeze(obj[prop]);
}
});
return obj;
}
- 注意:不处理
Symbol键,但常规配置极少用到 - 对
Date、RegExp等内置对象,typeof返回'object',但它们不可被Object.freeze安全冻结,建议配置中只用 plain object / array / primitive - ESLint 可配
no-param-reassign+no-const-assign辅助检测运行时误改
在 Node.js 或浏览器中锁定全局配置的最佳时机
必须在所有模块加载、任何代码访问该配置之前完成冻结。否则,一旦某个模块缓存了未冻结的引用(比如 const cfg = require('./config') 后再 freeze),其他模块拿到的仍是原始可变对象。
- Node.js:在入口文件(如
index.js)最顶部加载并冻结,再require其他模块 - 浏览器:在
<script type="module"></script>的顶层作用域立即执行,或通过import后立刻冻结(注意 ESM 的静态导入顺序) - 不要在导出时冻结:写
export default Object.freeze({...})是错的——如果对象里有嵌套,还是浅冻结;且无法保证 import 方拿到的是冻结后版本(某些打包器会优化掉)
冻结后仍可能被绕过的边界情况
Object.freeze 不是内存级防护,只是让 JS 引擎拒绝常规赋值和属性定义操作。开发者仍可通过某些间接方式“突破”语义限制,虽然不推荐,但得知道它们存在:
- 用
Object.defineProperty在非严格模式下强行重定义属性(但会静默失败或抛错,取决于环境) - 替换整个变量绑定:比如
CONFIG = {...}—— 这不是改对象,而是改绑定,Object.freeze完全不管 - 通过原型链注入:若配置对象继承自某可写原型,可改原型上的属性影响实例行为(所以应确保配置是
Object.create(null)或至少无污染原型)
真正防住意外修改,靠的是冻结 + const 声明 + 模块封装 + 代码审查。单靠 Object.freeze 就想一劳永逸,容易在深层嵌套或动态构造场景翻车。










