← 中文资源

简体中文版本

带循环保护的深度冻结

递归冻结数组与对象,兼容循环引用、访问器与无法冻结元素的类型化数组。

阅读英文原文

功能说明

该示例在 Object.freeze 只能冻结一层的基础上做递归冻结,常用于把配置、常量表或从接口拿到的数据变成只读结构,尽早暴露意外写入。实现用 WeakSet 记录已访问对象,因此自引用或互相引用的结构不会无限递归。函数返回原对象引用,便于链式使用,同时保持与 Object.freeze 一致的返回值语义。

关键代码

入口先放过 null 与所有原始类型,函数也会被冻结,因为函数同样是对象。WeakSet 在递归前加入当前对象,重复访问直接返回。类型化数组与 DataView 通过 ArrayBuffer.isView 识别并跳过,因为带元素的类型化数组调用 Object.freeze 会抛 TypeError。遍历使用 Reflect.ownKeys 以覆盖 Symbol 键,再读取属性描述符,只递归数据属性,避免触发 getter 的副作用。

使用注意

Object.freeze 是浅层且不可逆的,冻结后的对象在严格模式下写入会抛错,非严格模式则静默失败。Map 与 Set 的内部条目不受 Object.freeze 约束,仍可通过 set、add 修改,需要真正不可变时应改用持久化数据结构或在封装层拦截。访问器属性不会被递归,也无法阻止其返回值变化。已冻结对象如果子树未冻结,本实现仍会遍历并冻结子树。

验证方式

冻结嵌套对象与数组,断言每一层 Object.isFrozen 为真;构造带自引用的对象,确认调用不栈溢出;传入带元素的 Uint8Array,断言不抛错且值仍可写;定义带 getter 的对象,确认 getter 未被调用;用 Symbol 键挂载嵌套对象,验证 Symbol 属性也被冻结。

代码示例

export function deepFreeze(value, seen = new WeakSet()) {
  if (value === null || (typeof value !== "object" && typeof value !== "function")) {
    return value;
  }
  if (seen.has(value)) return value;
  seen.add(value);
  if (ArrayBuffer.isView(value)) return value;

  for (const key of Reflect.ownKeys(value)) {
    const descriptor = Object.getOwnPropertyDescriptor(value, key);
    if (descriptor && "value" in descriptor) {
      deepFreeze(descriptor.value, seen);
    }
  }
  return Object.freeze(value);
}