展开把一个对象铺进另一个对象
对象展开语法,也就是对象前面的三个点,会把这个对象自身可枚举的属性复制到外层的对象字面量中。要克隆、合并对象,或者在不改动原对象的情况下生成修改后的副本,这是最简短的写法。
copy 的键和值与 user 相同,但它是另一个对象。修改其中一个不会影响另一个,至少在顶层是这样。稍后我们会再谈这个限制。
可以这样理解:{ ...obj } 的意思是“把 obj 的属性倒进这个新的对象字面量”。在展开旁边写的任何内容都会成为结果的一部分。
一步完成克隆和覆盖
最常见的写法是先展开一个对象,再添加或覆盖几个属性。后面的键优先,所以先展开、后覆盖:
user 没有被改动。updated 是一个新对象,其中的 role 被替换了。这种不可变更新的写法在现代 JavaScript 中随处可见:React 的状态更新函数、Redux 的 reducer,以及任何不想原地修改数据的代码。
把顺序反过来,效果也会相反:
这里 role: "guest" 在前面,所以被 user.role 覆盖了。当你想提供可以被展开对象覆盖的默认值时,这种写法很方便。
合并对象
把两个(或更多)对象展开到一个新的字面量中,就能合并它们。遇到同名的键时,后面的对象会覆盖前面的:
theme 和 fontSize 来自 userPrefs;debug 则沿用 defaults 中的值。三个对象?四个?规则一样:从左往右读,最后写入的优先。
这是 Object.assign({}, defaults, userPrefs) 的现代替代写法。两者效果相同,但展开写法更易读,也不会诱使你犯 Object.assign(defaults, userPrefs) 这种常见错误,因为它会修改 defaults。
展开是浅拷贝
这正是让人上当的地方。展开只复制对象的顶层属性。如果某个属性的值本身是对象或数组,复制的是引用,而不是内容。
修改 copy.address.city 也改变了 user.address.city,因为两个对象共享同一个 address 对象。展开只给了我们一个新的外层包装。
要修改嵌套的内容时,需要展开你想修改的每一层:
要对任意数据真正深度克隆,就用 structuredClone(obj)。它能处理嵌套对象、数组、日期、Map 和 Set,而且所有现代运行时都内置了它。
展开与剩余
它们共用三个点,作用却相反。展开是拆开,剩余是收集。
经验法则:如果 ... 在 = 的左侧(解构),它就是剩余语法。如果它在右侧的对象或数组字面量中,它就是展开语法。
以不可变的方式删除属性
把剩余解构和展开结合起来,就能干净地生成一个删掉某个键的对象副本,不需要 delete,也不会修改原对象:
tempToken 被取出放进它自己的变量(你可以忽略它),其余的属性都进入 safe。原来的 user 没有被改动。
展开不会复制的东西
有几个值得了解的细节:
- 不可枚举的属性不会被复制。你创建的属性大多默认是可枚举的,但用
Object.defineProperty定义的属性和某些内置属性不是。 - 原型不会被复制。
{ ...instance }给你的是一个普通对象,而不是原来那个类的实例。定义在类原型上的方法不会出现在副本上。 - getter 会被求值。 展开一个带 getter 的对象时,会调用一次 getter,并把返回值作为普通属性存到新对象上。
copy 有 x 和 y,但它是一个普通对象:distance 位于 Point.prototype 上,而展开不会碰到它。如果需要克隆一个类的实例,通常要由这个类自己提供 clone 方法。
下一章:数组方法
展开是不可变数据工具箱中的一件工具。另一件重要工具是数组方法:map、filter、reduce 等,它们会生成新数组,而不是修改原数组。这就是下一页的内容。
常见问题
JavaScript 中 ...obj 有什么作用?
在对象字面量中,...obj 会把该对象自身所有可枚举的属性复制到新对象里。{ ...user } 会生成一个新对象,键和值都与 user 相同。这是在不修改原对象的前提下克隆或合并对象的标准写法。
JavaScript 中如何合并两个对象?
把两个对象都展开到一个新的对象字面量里:const merged = { ...a, ...b }。b 中的属性会覆盖 a 中同名的键,因为后面的属性优先。它等价于 Object.assign({}, a, b),但读起来更清晰。
对象展开是深拷贝吗?
不是。对象展开是浅拷贝:它复制顶层属性,但嵌套的对象和数组仍然按引用共享。修改 copy.address.city 也会修改 original.address.city。要真正深拷贝,请使用 structuredClone(obj)。
展开语法和剩余语法有什么区别?
它们看起来一样(...),作用却相反。展开把对象或数组拆开成单独的属性或元素:{ ...user }。剩余把剩下的部分收集到一个变量里:const { name, ...others } = user。看上下文就能分辨你面对的是哪一种。