如何用 Object.getOwnPropertyDescriptors 完美克隆包含 Getter/Setter 的复杂对象

Object.getOwnPropertyDescriptors 为什么能拿到 getter/setter
许多开发者存在一个普遍的误解,认为 Object.assign 或展开运算符 {...obj} 可以实现对象的完全复制。实际上,这两种方法仅复制属性的“值”,对于访问器属性(即包含 get 和 set 函数的属性)则完全失效,因为这些访问器函数本身不会被遍历和复制。
解决这一问题的核心在于 Object.getOwnPropertyDescriptors 方法。它返回的不是属性值,而是每个属性的完整描述符对象。这个描述符对象包含了 get、set、enumerable、configurable、writable 等所有元数据。因此,它是精确复制访问器属性行为的唯一可靠途径,也是实现深度克隆的关键步骤。
克隆时必须用 Object.defineProperties 而非 Object.assign
如果错误地使用 Object.assign 进行克隆,会发生什么?它会将源对象的 getter 函数当作普通函数立即调用一次,然后将返回值作为一个静态的、不可变的数据属性赋值给目标对象。同时,setter 函数会完全丢失,导致克隆后的对象失去响应式能力。
正确的克隆流程是:首先使用 Object.getOwnPropertyDescriptors 获取源对象的所有属性描述符,然后通过 Object.defineProperties 方法将这些描述符精确地应用到新的空对象上,从而实现属性的“复刻”,包括其访问器行为:
const source = {
_x: 10,
get x() { return this._x * 2; },
set x(v) { this._x = v / 2; }
};
const descriptors = Object.getOwnPropertyDescriptors(source);
const clone = Object.defineProperties({}, descriptors);
// ✅ 正确:clone.x 是响应式的访问器属性,修改 clone.x 会触发 setter
// ❌ 错误:Object.assign({}, source) 得到的是 { x: 20 } —— 一个静态值,无访问器功能
深层克隆需递归处理,但 descriptor 不含原型链信息
需要明确一个关键点:Object.getOwnPropertyDescriptors 仅作用于对象自身的(own)属性,不包含从原型链继承而来的属性,也不处理对象内部嵌套的复杂数据结构。因此,要实现一个健壮的深克隆函数,必须引入递归逻辑。
实现深度克隆的递归思路如下:
- 对于待克隆的普通对象(非 null),首先调用
Object.getOwnPropertyDescriptors获取其所有自身属性的描述符。 - 遍历每个描述符,对其
value字段进行判断:如果该值本身是对象或数组,则需要对这个值进行递归克隆,并将克隆结果作为新的value。 - 对于描述符中的
get或set函数引用,应直接保留,无需也无法进行深克隆。 - 特别注意:
Object.defineProperties只负责定义属性,不会自动设置对象的原型链。如果需要完整克隆,应在定义属性后,使用Object.setPrototypeOf(clone, Object.getPrototypeOf(source))来显式设置克隆对象的原型。
容易漏掉的三个坑
理解了基本原理后,在实际编码中仍需警惕以下几个常见陷阱,它们常常导致克隆结果与预期不符:
- 属性特性被忽略:属性描述符中的
enumerable(可枚举性)和configurable(可配置性)等特性默认值为false。如果源对象的某个属性这些特性为true,但在克隆时未正确传递,克隆后的属性可能会变得不可枚举(例如在for...in循环中不可见)或不可删除。 - Symbol 键被遗忘:
Object.getOwnPropertyDescriptors默认只返回字符串键的属性描述符。对于使用 Symbol 作为键名的属性,必须额外使用Object.getOwnPropertySymbols方法获取其描述符,并将其合并到克隆流程中,否则这些属性会丢失。 - 只读访问器被篡改:如果源对象定义了一个只有
get而没有set的只读访问器属性,克隆后理应保持其只读特性。然而,如果在调用Object.defineProperties时错误地传递了set: undefined,JavaScript 引擎会将其静默转换为一个普通的、可写的数据属性,从而破坏了原有的只读约束。
总而言之,实现一个“完美”的复杂对象克隆,并非依赖某个单一的 API 就能完成。它要求开发者深刻理解属性描述符的完整结构,并在使用 Object.defineProperties 时,一丝不苟地保留每个描述符字段的原始语义。任何细微的疏忽,都可能在后续的属性修改、遍历或特性检查中暴露问题,导致克隆对象行为异常。
