主流深拷贝工具如JSON.stringify()→parse()、structuredClone()、Lodash _.cloneDeep()和fast-copy默认跳过不可枚举属性;仅fast-copy开启strict模式、手写描述符遍历或PHP DeepCopy等方案能完整保留。

不可枚举属性(non-enumerable property)在深拷贝中常被静默忽略,导致拷贝结果与原始对象行为不一致——比如 Object.defineProperty(obj, 'secret', { value: 42, enumerable: false }) 定义的属性,在多数深拷贝方案里根本不会出现在副本中。这不是 bug,而是设计取舍,但开发者往往直到运行时才发现“丢了东西”。
哪些工具默认跳过不可枚举属性?
绝大多数主流方案都只遍历可枚举自有属性(for...in 或 Object.keys()),包括:
- JSON.stringify() → parse():完全无视不可枚举属性,也不处理 Symbol 键
-
structuredClone():仅复制可枚举自有属性,忽略
enumerable: false和原型链上的属性 -
Lodash _.cloneDeep():默认不拷贝不可枚举属性;需配合
_.getOwnPropertyDescriptors()手动扩展才可覆盖 -
fast-copy(标准模式):为性能默认跳过,仅在“严格模式”(
strict: true)下启用完整属性描述符拷贝
如何检测一个属性是否不可枚举?
不能只靠 for...in 或 Object.keys() ——它们天然过滤掉不可枚举项。正确方式是:
- 用
Object.getOwnPropertyNames(obj)获取所有自有属性名(含不可枚举) - 用
Object.getOwnPropertySymbols(obj)获取自有 Symbol 属性 - 结合
Object.getOwnPropertyDescriptor(obj, key)查看enumerable字段值 - 注意:
in操作符和hasOwnProperty()不反映枚举性,只反映存在性
真正保留不可枚举属性的深拷贝路径
只有明确支持“属性描述符完整复刻”的方案才能可靠还原:
-
fast-copy 开启 strict 模式:
copy(obj, { strict: true })会调用Object.getOwnPropertyDescriptors()并用Object.defineProperties()重建 -
手写递归 + 描述符遍历:在 clone 函数中替换
for...in为Object.getOwnPropertyNames()+getOwnPropertyDescriptors(),再逐个 define - PHP 的 DeepCopy 库:通过反射获取全部属性(含私有、受保护、不可枚举),天然支持,无需额外配置
为什么多数库不默认保留?
不是能力不足,而是权衡结果:
- 性能开销:获取并重建描述符比简单遍历慢 3–5 倍
- 语义模糊:不可枚举属性常用于内部状态(如 React 的
$$typeof)、代理标记或私有字段,拷贝可能破坏封装或引发意外行为 - 兼容性风险:某些框架依赖不可枚举属性的“不传播”特性,强制拷贝反而导致错误


















