object.freeze仅浅冻结,需递归实现完全不可变;它只冻结第一层属性,嵌套对象或数组内部仍可修改;递归冻结需遍历自有属性、区分类型、处理循环引用并跳过非plain对象。

Object.freeze 本身只做浅冻结,要实现“完全不可变”,必须手动递归处理嵌套对象和数组。
为什么 Object.freeze 不够用
它只冻结第一层属性:禁止添加、删除、修改属性,但对属性值是对象或数组时,不冻结其内部。比如:
const obj = { a: 1, nested: { b: 2 } };Object.freeze(obj);
obj.nested.b = 3; // ✅ 仍能修改 —— 这就是浅冻结的局限
递归冻结的核心逻辑
遍历对象所有自有可枚举属性(包括数组索引),对每个值判断类型:如果是普通对象或数组,先递归冻结,再调用 Object.freeze。
- 只冻结 plain object 和 Array,跳过 null、原始值、Date、RegExp、Function 等内置类型(它们本就不该被“修改结构”)
- 需检测循环引用,否则会无限递归 —— 可用 WeakMap 记录已处理对象
- 冻结前先确保目标是 object 类型(排除 string/number/boolean 等)
一个安全可用的实现
以下是一个兼顾健壮性和简洁性的递归冻结函数:
function deepFreeze(obj) {if (obj == null || typeof obj !== 'object') return obj;
// 避免重复冻结和循环引用
const seen = new WeakMap();
function freeze(item) {
if (item == null || typeof item !== 'object') return item;
if (seen.has(item)) return item;
seen.set(item, true);
// 区分数组和普通对象
if (Array.isArray(item)) {
item.forEach((_, i) => freeze(item[i]));
} else {
Object.getOwnPropertyNames(item).forEach(prop => freeze(item[prop]));
}
return Object.freeze(item);
}
return freeze(obj);
}
使用注意点
冻结后无法撤销,且不会报错(静默失败)。实际开发中建议:
- 仅在创建配置、常量数据或 reducer 初始状态时使用,避免对运行时频繁变动的对象调用
- 冻结后仍可通过 Object.isFrozen 检查是否生效
- 若需部分可变性,考虑用 Immutable.js 或 immer 等更灵活的方案
- 注意 Symbol 属性也会被冻结,但 Object.getOwnPropertyNames 不返回它们 —— 如需支持,改用 Reflect.ownKeys
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











