文章目录

JavaScript Object.freeze深层冻结对象的递归实现

发布于 2026-04-20 07:23:07 · 浏览 4 次 · 评论 0 条

JavaScript Object.freeze深层冻结对象的递归实现

在JavaScript开发中,使用 Object.freeze() 可以防止对象被修改。然而,Object.freeze() 只能进行“浅层冻结”:它只会冻结对象自身的属性,如果某个属性的值是另一个对象(嵌套对象),那个内部对象仍然是可以被修改的。为了实现“深层冻结”,必须编写一个递归函数来遍历并冻结所有层级的子对象。

以下是将普通对象转换为完全不可变对象的分步实操指南。


第一步:理解浅层冻结的局限性

在编写深层冻结代码前,先确认为何需要它。通过以下操作验证默认行为的不足。

  1. 打开 浏览器的开发者工具控制台(或 Node.js 环境)。
  2. 定义 一个包含嵌套结构的对象 data
const data = {
    id: 1,
    details: {
        name: "Alice",
        scores: [10, 20, 30]
    }
};
  1. 执行 浅层冻结命令:
Object.freeze(data);
  1. 尝试修改 第一层属性 id
data.id = 99;
  1. 查看 data.id 的值。在严格模式下("use strict"),这会报错;在非严格模式下,修改无效,值仍为 1

  2. 尝试修改 嵌套对象属性 details.name

data.details.name = "Bob";
  1. 查看 data.details.name 的值。你会发现值变成了 "Bob"。这说明 Object.freeze() 并没有保护内部的 details 对象。

第二步:梳理递归冻结的逻辑流程

要解决这个问题,需要“先里后外”地处理:先冻结最深层的属性,再一层层向外冻结。以下是其逻辑判断流程。

graph TD A["输入对象 obj"] --> B{"obj 是对象
且不为 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 递归函数

根据上述逻辑,编写核心函数。我们需要获取对象的所有属性,检查每个属性值是否为对象,如果是,就对该值调用自身,最后冻结当前对象。

  1. 创建 一个名为 deepFreeze 的函数,接收一个参数 obj

  2. 编写 递归逻辑代码:

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,测试嵌套修改是否被拦截。

  1. 重置 测试数据 data(确保使用一个新的对象):
const newData = {
    id: 1,
    details: {
        name: "Alice",
        meta: { role: "admin" }
    }
};
  1. 调用 deepFreeze 函数处理 newData
deepFreeze(newData);
  1. 开启 严格模式(为了更容易看到报错效果):
"use strict";
  1. 尝试修改 深层嵌套属性 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>' 的错误信息,证明深层对象也被成功冻结。

  1. 尝试修改 数组元素(如果对象包含数组):
const config = {
    items: [1, 2, 3]
};
deepFreeze(config);
config.items[0] = 99; // 报错

由于数组在 JavaScript 中本质上是对象,deepFreeze 同样会冻结数组,阻止通过索引修改元素。


第五步:处理特殊情况的补充说明

在实际工程应用中,为了确保代码的健壮性,还需要注意以下边界情况。

  1. 识别 循环引用。如果对象属性引用了对象自身(例如 obj.self = obj),上述递归函数会导致“栈溢出”。
  2. 实现 弱引用映射来防止循环引用(进阶场景)。如果在极其复杂的对象结构中,需引入 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 的初始化。

通过以上步骤,你已在代码中实现了一个健壮的、支持深层嵌套和防循环引用的对象冻结方案,确保数据状态在运行期间绝对不可变。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文