Object.assign 只是个简单的“复制”工具,把源对象的属性一股脑搬到目标对象上就完事。但事实上,它对访问器属性(即通过 get/set 定义的属性)的处理方式,远没有你想象的那么“智能”。直白点说,它只复制属性值,不会复制“行为”——这正是许多 JavaScript 开发者容易踩坑的地方。

先明确一个核心结论:Object.assign 在拷贝过程中,既不会调用源对象上的 getter,也不会在目标对象上保留 setter。它执行的是纯粹的“属性值”浅拷贝,对访问器属性的处理机制非常特殊,也极易被忽略,理解这一点对于掌握 JavaScript 对象拷贝 的深层原理至关重要。
访问器属性会被转为普通数据属性
如果源对象的某个属性是用 get / set 定义的访问器属性,Object.assign 会读取该属性当前的返回值——即调用一次 getter 拿到结果,然后把这个值作为普通数据属性直接赋给目标对象。最终结果是:目标对象上对应的键变成了一个可读写的普通数据属性,彻底丢失了 getter/setter 行为。换句话说,Object.assign 浅拷贝 并不会保留属性描述符中的 get 与 set 定义。
- 源对象的 getter 不会被复制,只有它的返回值以“快照”方式写入目标对象
- 源对象的 setter 完全丢失,目标对象上的同名属性再也无法触发任何 setter 逻辑
- 后续你对目标对象该属性的修改,完全不会影响源对象,也不会触发任何访问器行为
无法继承或还原原始的访问器定义
这种表现背后的机制其实不复杂:Object.assign 基于 [[Get]] 和 [[Set]] 内部操作,而非 Object.getOwnPropertyDescriptor 搭配 Object.defineProperty。因此它不会保留属性的 configurable、enumerable、writable 等特性,更不用说 get / set 函数本身了。如果你想实现 保留访问器属性的拷贝,必须采用更底层的方法。
- 即使源属性是不可枚举的,只要它能被
for...in或Object.keys遍历到(即enumerable: true),就会被拷贝 - 若要完整保留访问器定义,必须手动使用
Object.getOwnPropertyDescriptors配合Object.defineProperties来实现
实际例子说明行为差异
来看一个具体的示例,直观感受 JavaScript 访问器属性拷贝 的差异:
const src = {
_value: 42,
get foo() { return this._value * 2; },
set foo(v) { this._value = v / 2; }
};
const target = {};
Object.assign(target, src);
console.log(target.foo); // 84(getter 被执行一次,结果被固化)
target.foo = 100;
console.log(target.foo); // 100(已变成普通数据属性,不再触发 setter)
console.log(src._value); // 42(未改变,setter 没生效)
看到了吗?target.foo 现在是一个纯数据属性,与 src 的访问器逻辑彻底脱钩。无论你后续如何修改它,都不会影响源对象,因为 setter 已经彻底丢失了。这个例子清楚地展示了 Object.assign 对 getter/setter 的处理 与直觉之间的差距。
需要保留访问器时的替代方案
如果你需要进行深拷贝,或者想要保留访问器以及属性描述符等元信息,那就得换一种方式,不要直接使用 Object.assign:
- 使用
Object.getOwnPropertyDescriptors(src)获取完整的属性描述符 - 再通过
Object.defineProperties(target, descriptors)将它们复制到目标对象 - 注意:这仍然是浅拷贝,如果嵌套对象或访问器内部逻辑较为复杂,还需要额外处理才能实现真正意义上的 JavaScript 深拷贝保留访问器
