JavaScript Object.freeze深层冻结对象的递归实现
在JavaScript开发中,使用 Object.freeze() 可以防止对象被修改。然而,Object.freeze() 只能进行“浅层冻结”:它只会冻结对象自身的属性,如果某个属性的值是另一个对象(嵌套对象),那个内部对象仍然是可以被修改的。为了实现“深层冻结”,必须编写一个递归函数来遍历并冻结所有层级的子对象。
以下是将普通对象转换为完全不可变对象的分步实操指南。
第一步:理解浅层冻结的局限性
在编写深层冻结代码前,先确认为何需要它。通过以下操作验证默认行为的不足。
- 打开 浏览器的开发者工具控制台(或 Node.js 环境)。
- 定义 一个包含嵌套结构的对象
data:
const data = {
id: 1,
details: {
name: "Alice",
scores: [10, 20, 30]
}
};
- 执行 浅层冻结命令:
Object.freeze(data);
- 尝试修改 第一层属性
id:
data.id = 99;
-
查看
data.id的值。在严格模式下("use strict"),这会报错;在非严格模式下,修改无效,值仍为1。 -
尝试修改 嵌套对象属性
details.name:
data.details.name = "Bob";
- 查看
data.details.name的值。你会发现值变成了"Bob"。这说明Object.freeze()并没有保护内部的details对象。
第二步:梳理递归冻结的逻辑流程
要解决这个问题,需要“先里后外”地处理:先冻结最深层的属性,再一层层向外冻结。以下是其逻辑判断流程。
且不为 null?"} B -- 否 --> C["直接返回 obj
(基础类型无需冻结)"] B -- 是 --> D["调用 Object.freeze obj"] D --> E["获取 obj 的所有属性键名"] E --> F["遍历每一个属性键"] F --> G["获取当前属性的值 value"] G --> H{"value 是对象
且不为 null?"} H -- 是 --> I["递归调用 deepFreeze value"] I --> F H -- 否 --> F F --> J{"遍历结束?"} J -- 否 --> F J -- 是 --> K["返回冻结后的对象"]
第三步:编写 deepFreeze 递归函数
根据上述逻辑,编写核心函数。我们需要获取对象的所有属性,检查每个属性值是否为对象,如果是,就对该值调用自身,最后冻结当前对象。
-
创建 一个名为
deepFreeze的函数,接收一个参数obj。 -
编写 递归逻辑代码:
function deepFreeze(obj) {
// 1. 检查传入值是否为对象,或者是否为 null
// typeof null === 'object',所以必须单独排除 null
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 2. 获取对象自身的所有属性名(包括不可枚举的属性)
const propNames = Object.getOwnPropertyNames(obj);
// 3. 遍历所有属性
for (const name of propNames) {
const value = obj[name];
// 4. 如果属性值也是对象(且不为 null),则递归冻结
if (value && typeof value === 'object') {
deepFreeze(value);
}
}
// 5. 冻结自身(确保无法新增或删除现有属性)
return Object.freeze(obj);
}
代码关键点解析:
Object.getOwnPropertyNames(obj):这比Object.keys()更强大,因为它能获取到不可枚举的属性,确保全方位冻结。value && typeof value === 'object':这个判断排除了null,因为typeof null也是"object",但null不能被冻结。
第四步:验证深层冻结效果
现在用新的函数替换掉原来的 Object.freeze,测试嵌套修改是否被拦截。
- 重置 测试数据
data(确保使用一个新的对象):
const newData = {
id: 1,
details: {
name: "Alice",
meta: { role: "admin" }
}
};
- 调用
deepFreeze函数处理newData:
deepFreeze(newData);
- 开启 严格模式(为了更容易看到报错效果):
"use strict";
- 尝试修改 深层嵌套属性
newData.details.meta.role:
try {
newData.details.meta.role = "user";
} catch (e) {
console.error("修改失败:", e.message);
}
控制台将输出类似 Cannot assign to read only property 'role' of object '#<Object>' 的错误信息,证明深层对象也被成功冻结。
- 尝试修改 数组元素(如果对象包含数组):
const config = {
items: [1, 2, 3]
};
deepFreeze(config);
config.items[0] = 99; // 报错
由于数组在 JavaScript 中本质上是对象,deepFreeze 同样会冻结数组,阻止通过索引修改元素。
第五步:处理特殊情况的补充说明
在实际工程应用中,为了确保代码的健壮性,还需要注意以下边界情况。
- 识别 循环引用。如果对象属性引用了对象自身(例如
obj.self = obj),上述递归函数会导致“栈溢出”。 - 实现 弱引用映射来防止循环引用(进阶场景)。如果在极其复杂的对象结构中,需引入
WeakSet记录已处理过的对象。
以下是支持防循环引用的增强版实现:
function deepFreeze(obj, frozenSet = new WeakSet()) {
// 如果不是对象或为 null,直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 如果该对象已经被冻结过,直接返回,避免重复处理和循环引用
if (frozenSet.has(obj)) {
return obj;
}
// 标记该对象已处理
frozenSet.add(obj);
// 冻结自身(先冻结自身也可以,或者先递归后冻结,通常逻辑先递归)
// 这里我们先获取属性,递归处理子属性,最后冻结自身
const propNames = Object.getOwnPropertyNames(obj);
for (const name of propNames) {
const value = obj[name];
// 递归调用,传入同一个 frozenSet
if (value && typeof value === 'object') {
deepFreeze(value, frozenSet);
}
}
return Object.freeze(obj);
}
使用增强版函数:
直接调用 deepFreeze(targetObj) 即可,内部会自动处理 WeakSet 的初始化。
通过以上步骤,你已在代码中实现了一个健壮的、支持深层嵌套和防循环引用的对象冻结方案,确保数据状态在运行期间绝对不可变。

暂无评论,快来抢沙发吧!