功能说明
该示例在 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);
}