Menu

JavaScript 剩余参数与展开语法:... 运算符详解

JavaScript 中 ... 运算符的用法:用剩余参数收集实参,用展开语法展开数组和对象,以及各自适用的场景。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

同一个语法,两种用途

三个点 ... 在现代 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 序列化。

Coddy 编程语言插图

和 Coddy 一起学编程

开始