属性递归保护不能直接实现“只读锁定”,仅对自身属性设writable: false,不约束嵌套对象、数组元素、新增属性及原型链;深拷贝后需递归冻结或proxy拦截才能真正只读。

属性递归保护不能直接实现“只读锁定”,它只是对对象自身属性设置 Object.defineProperty 的 writable: false,但对嵌套对象、数组元素、新增属性或原型链上的属性无约束力。深拷贝副本中的原始字段是否敏感,取决于拷贝方式与后续防护策略的组合——关键不在“递归保护”本身,而在“何时保护”“保护什么”以及“如何拦截非法修改”。
深拷贝后立即冻结(浅层冻结不等于深层只读)
使用 structuredClone 或 JSON.parse(JSON.stringify()) 得到副本后,若仅调用 Object.freeze(obj),只会冻结顶层对象,其内部的普通对象、数组仍可被修改。
- 必须递归遍历所有嵌套对象和数组,对每个可枚举自有属性的对象调用
Object.freeze - 注意:
freeze不影响 Symbol 属性、不可枚举属性,也不阻止添加新属性(除非同时Object.seal或Object.preventExtensions) - 示例:对数组需单独处理——
Array.isArray(val) && Object.freeze(val),再递归其元素
用 Proxy 实现真正动态只读拦截
冻结是静态防护,无法捕获运行时对未冻结子对象的修改。Proxy 可在访问/赋值时实时拦截,适合对敏感字段做细粒度控制。
- 创建只读代理时,
set拦截器应始终返回false(严格模式下抛错),defineProperty和deleteProperty同样拦截 - 对嵌套结构,需在
get中判断返回值是否为对象,并自动包装为只读 Proxy(即“懒递归代理”) - 注意避免代理循环:缓存已代理对象,相同引用复用同一 Proxy
结合字段白名单 + 不可变数据结构
若只需保护特定字段(如 id、createdAt),不必全量冻结或代理整个树。
- 在深拷贝后,用
Object.defineProperty单独锁定目标字段:writable: false, configurable: false - 对数组类敏感字段(如
permissions),可转为Object.freeze([...arr])并替换原属性 - 更进一步,用
immer或immutable-js构建不可变数据模型,天然杜绝就地修改
警惕常见失效场景
所谓“只读锁定”容易因忽略边界情况而形同虚设:
- Date、RegExp、Map、Set 等内置类型在 JSON 拷贝中会丢失或降级(Date → 字符串),需用
structuredClone或自定义拷贝逻辑 - 函数、undefined、NaN、Infinity 在 JSON 中被忽略或转为 null,导致副本结构失真,保护失去意义
- Proxy 无法代理非对象(原始值),若敏感字段是字符串或数字,需将其包裹为对象(如
{ value: 'xxx' })再代理










