同一个语法,两种用途
三个点 ... 在现代 JavaScript 代码中随处可见,根据出现的位置,它做的是两件相反的事。一旦看清规律,每一处 ... 都可以归入以下两类之一:
- 剩余(rest):在接收一方的
...name,把多个值收集到一个数组或对象中。 - 展开(spread):在给出一方的
...value,把数组或对象展开成一个个独立的部分。
这就是全部的思维模型。本页剩下的内容只是两者的示例,以及你会反复用到的写法。
剩余参数:收集实参
函数定义中的剩余参数,会把任意数量的实参收集到一个真正的数组中:
nums 就是一个普通数组。你可以对它 .map、.filter,查看它的 .length,把它传给另一个函数,数组能做的都可以。
剩余参数可以和普通参数一起使用,但剩余参数必须放在最后:
label 接收第一个实参,之后的所有实参都进入 items。把剩余参数放在最后以外的任何位置,都是语法错误。
剩余参数与旧的 arguments 对象
较老的 JavaScript 代码会在普通函数内部使用一个叫 arguments 的特殊变量。它看起来像数组,但并不是,这意味着数组方法不能直接用在它上面。剩余参数可以干净地取代它:
箭头函数甚至没有 arguments 对象,所以在箭头函数中,剩余参数是接收可变数量实参的唯一方式。在新代码中,优先使用 ...args。
在函数调用中展开
展开做的正好相反:它把一个数组拆开,在调用处变成一个个独立的实参。
Math.max 接收的是一个个数字,而不是数组。在有展开语法之前,你得写 Math.max.apply(null, nums)。现在只要一个 ... 就搞定了。
注意,完全相同的 ...,在函数定义中是剩余,在函数调用中是展开:位置决定了它是哪一种。
在数组字面量中展开
在数组字面量中展开,可以复制或合并数组:
[...a] 会给你一个元素相同的新数组。想排序或修改却不想动原数组时,这很有用:
scores 没有被改动,因为 .sort 是在副本上运行的。习惯虽小,但在写不该有意外副作用的代码时,回报很大。
在对象字面量中展开
展开同样适用于普通对象,把它们的属性合并到一个新对象中:
后面的键优先。updates.age 覆盖了 user.age,city 也一并加了进来。展开的顺序决定了结果,叠加默认值和覆盖值时要记住这一点:
默认值在前,用户的选择在后。用户在 fontSize 上胜出,theme 则沿用默认值。
浅拷贝陷阱
展开只复制一层。嵌套的对象和数组仍然在原对象和副本之间共享:
两个数组都显示了新标签,因为 copy.tags 和 original.tags 是同一个数组。展开并没有克隆嵌套的列表,只是复制了引用。
要对普通数据做真正的深拷贝,就用 structuredClone:
现在两个数组互相独立了。structuredClone 内置于现代浏览器和 Node 中,能处理嵌套结构,只要“浅”不够用,它就是正确的选择。
解构中的剩余语法
剩余语法也能用在解构中,用来收集剩下的元素或属性:
取出几个字段、把其余的保留在一个对象里,是一种常见写法,可以用来转发 props、剔除敏感字段,或者构造数据的修改版本:
password 被取出(并被忽略);safe 保存其余的一切。没有修改原对象,也不需要手动复制。
快速回顾
- 在参数列表或解构模式中的
...name是剩余:它负责收集。 - 在函数调用、数组字面量或对象字面量中的
...value是展开:它负责展开。 - 展开得到的是浅拷贝。嵌套结构仍然共享。深拷贝用
structuredClone。 - 剩余参数是真正的数组,用它代替
arguments。 - 在对象字面量中,后面的展开会覆盖前面的,“默认值加覆盖值”就是这样实现的。
下一篇:闭包
JavaScript 中的函数不只是接收输入、返回输出,它们还会记住自己被定义时所在的作用域。这种记忆叫作闭包,它是回调、工厂函数以及下一页中大多数写法背后的机制。
常见问题
JavaScript 中剩余参数和展开语法有什么区别?
它们用的是同一个 ... 语法,但做的事情正好相反。剩余语法把多个值收集到一个数组里,出现在函数参数列表和解构中。展开语法把一个可迭代对象或对象拆成一个个部分,出现在函数调用、数组字面量和对象字面量中。如果 ... 在接收的一方,就是剩余;如果在给出的一方,就是展开。
函数中的剩余参数是怎样工作的?
像 function sum(...nums) 这样的剩余参数,会把传给函数的所有实参收集到一个名为 nums 的真正数组中。它必须是参数列表中的最后一个参数。与旧的 arguments 对象不同,剩余参数是一个真正的数组,所以 .map、.filter 和 .reduce 都能直接在它上面使用。
展开运算符是深拷贝吗?
不是。展开只复制一层,也就是浅拷贝。{ ...user } 会给你一个顶层键相同的新对象,但其中嵌套的对象和数组仍然是共享的引用。要深拷贝,用 structuredClone(value),如果是普通数据,也可以通过 JSON 序列化。