侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 195 篇文章
  • 累计收到 7 条评论

合并配置时,Object.assign 触发 setter

2026-10-15 / 0 评论 / 8 阅读

合并配置时,Object.assign 触发 setter

有些对象属性来自原型,而不是对象本身。把一份配置合进这种对象时,{ ...source } 和 Object.assign(target, source) 看起来都像复制,结果却可能相反:前者留下自有属性,后者调用原型上的 setter,值没有写进目标对象。

这个差别在 UI 状态代理、带访问器的配置对象里值得留意。只看表达式最后的对象容易漏掉副作用,尤其是 setter 只更新外部状态或做校验时。实测把一个带 value 字段的普通对象复制到两个目标上,展开结果持有 value: 42;assign 的目标只记录到一次 setter 调用,自身没有 value 属性。

为什么展开没有调用 setter?

对象展开创建新对象,并把源对象可枚举的自有属性作为数据属性放进去。它不会把源对象的原型带过去,也不会对新对象逐项执行普通赋值。以下面的例子来说,展开产物有一个普通的自有 value 属性,读出来是 42。

Object.assign 的目标对象则已经存在。对每个可枚举的自有源属性,它会把值写到目标对象上。这个写入经过目标的属性查找;若目标没有自有 value,但原型链上有 setter,setter 就会接到赋值。setter 可以记录值,也可以改动其他状态,甚至什么也不保存。调用结束后,目标上仍可能没有同名自有属性。代理目标也可能拦截写入;若这段逻辑依赖目标端校验或副作用,Object.assign 的行为就不能随手换成展开。

assign 为什么没留下自有属性?

下面这份本机 Node.js 脚本给目标对象装了继承 setter,断言两种行为后打印结果。运行方式是 node tools/verify-object-spread-setter.mjs。

对象展开 value: 42
对象展开有自有属性: true
Object.assign 返回目标对象: true
Object.assign 后目标有自有属性: false
setter 调用记录: setter:42

最容易误读的是 Object.assign 后的 false。这不表示赋值失败或抛错,而是赋值被 setter 接收了;setter 没有设置自有属性,所以随后检查不到键。Object.assign 仍然返回原目标对象,脚本也用严格相等确认了这一点。

若目标上已有自有数据属性,赋值会更新它,不会因原型上的同名 setter 而调用 setter。若目标上的同名自有属性本身是访问器,则会调用该对象自己的 setter。这里要分清的是「目标当前属性如何接收写入」,并非源对象是否有 setter。这个实验刻意让源对象只有一个普通数据属性,避免把读取源值时可能执行的 getter 混进结果。两种操作都会读取源属性的当前值,因此源属性若是 getter,读取阶段就可能先发生副作用。复制过去的也不是属性描述符,目标侧拿到的是值,而不是原访问器本身。对象展开语法里写 { ...source } 时,这些细节被语法藏得很轻,审查时容易只盯着花括号。

合并配置该选哪种写法?

需要构造一个不继承输入原型的新普通对象,且期望把枚举到的键变成数据属性时,对象展开更贴近这个意图。它也不因目标原型预置的 setter 而改变写入行为。若特意希望把值交给目标对象的 setter,或者需要原地改动目标,Object.assign 的赋值语义才合适。

这不是说展开对所有对象都更安全。源对象属性读取本身仍可能触发 getter,展开也只做浅复制,嵌套对象仍共享引用。我不建议把两种写法当作纯粹的风格替换,尤其目标对象是代理、类实例或继承了访问器时。先确定是否要触发目标端逻辑,再选写法,通常比代码短一行更重要。

    🤞 分享