2022 年 10 月 18 日

剩余参数和展开语法

许多 JavaScript 内置函数支持任意数量的参数。

例如

  • Math.max(arg1, arg2, ..., argN) – 返回参数中的最大值。
  • Object.assign(dest, src1, ..., srcN) – 将 src1..N 中的属性复制到 dest 中。
  • ……等等。

在本章中,我们将学习如何执行相同操作。此外,还将学习如何将数组作为参数传递给此类函数。

剩余参数 ...

无论如何定义函数,都可以使用任意数量的参数调用该函数。

如下所示

function sum(a, b) {
  return a + b;
}

alert( sum(1, 2, 3, 4, 5) );

由于“过多的”参数,不会出现错误。但当然,结果中只会计算前两个参数,因此上面代码中的结果是 3

可以使用三个点 ... 后跟将包含它们的数组的名称,将剩余的参数包含在函数定义中。这些点实际上表示“将剩余的参数收集到一个数组中”。

例如,将所有参数收集到数组 args

function sumAll(...args) { // args is the name for the array
  let sum = 0;

  for (let arg of args) sum += arg;

  return sum;
}

alert( sumAll(1) ); // 1
alert( sumAll(1, 2) ); // 3
alert( sumAll(1, 2, 3) ); // 6

我们可以选择将第一个参数作为变量获取,而只收集其余参数。

这里前两个参数进入变量,其余参数进入 titles 数组

function showName(firstName, lastName, ...titles) {
  alert( firstName + ' ' + lastName ); // Julius Caesar

  // the rest go into titles array
  // i.e. titles = ["Consul", "Imperator"]
  alert( titles[0] ); // Consul
  alert( titles[1] ); // Imperator
  alert( titles.length ); // 2
}

showName("Julius", "Caesar", "Consul", "Imperator");
剩余参数必须在末尾

剩余参数收集所有剩余参数,因此以下内容没有意义并会导致错误

function f(arg1, ...rest, arg2) { // arg2 after ...rest ?!
  // error
}

...rest 必须始终是最后一个。

“arguments” 变量

还有一个名为 arguments 的特殊类数组对象,它按其索引包含所有参数。

例如

function showName() {
  alert( arguments.length );
  alert( arguments[0] );
  alert( arguments[1] );

  // it's iterable
  // for(let arg of arguments) alert(arg);
}

// shows: 2, Julius, Caesar
showName("Julius", "Caesar");

// shows: 1, Ilya, undefined (no second argument)
showName("Ilya");

在过去,该语言中不存在剩余参数,而使用 arguments 是获取函数所有参数的唯一方法。它仍然有效,我们可以在旧代码中找到它。

但缺点是,尽管 arguments 既类似数组又可迭代,但它不是数组。它不支持数组方法,因此我们无法调用 arguments.map(...)

此外,它始终包含所有参数。我们无法部分捕获它们,就像我们使用剩余参数所做的那样。

因此,当我们需要这些特性时,则优先使用剩余参数。

箭头函数没有 "arguments"

如果我们从箭头函数访问 arguments 对象,它将从外部“普通”函数中获取它们。

这是一个例子

function f() {
  let showArg = () => alert(arguments[0]);
  showArg();
}

f(1); // 1

正如我们所记得的,箭头函数没有自己的 this。现在我们知道它们也没有特殊的 arguments 对象。

展开语法

我们刚刚看到如何从参数列表中获取数组。

但有时我们需要做完全相反的事情。

例如,有一个内置函数 Math.max,它返回列表中最大的数字

alert( Math.max(3, 5, 1) ); // 5

现在假设我们有一个数组 [3, 5, 1]。我们如何使用它调用 Math.max

“按原样”传递它不起作用,因为 Math.max 期望一个数字参数列表,而不是一个数组

let arr = [3, 5, 1];

alert( Math.max(arr) ); // NaN

而且我们肯定不能在代码 Math.max(arr[0], arr[1], arr[2]) 中手动列出项,因为我们可能不确定有多少项。当我们的脚本执行时,可能有很多,也可能没有。那会变得很丑陋。

展开语法 来救援!它看起来类似于剩余参数,也使用 ...,但恰恰相反。

当在函数调用中使用 ...arr 时,它将可迭代对象 arr “展开”为参数列表。

对于 Math.max

let arr = [3, 5, 1];

alert( Math.max(...arr) ); // 5 (spread turns array into a list of arguments)

我们还可以通过这种方式传递多个可迭代对象

let arr1 = [1, -2, 3, 4];
let arr2 = [8, 3, -8, 1];

alert( Math.max(...arr1, ...arr2) ); // 8

我们甚至可以将展开语法与正常值结合起来

let arr1 = [1, -2, 3, 4];
let arr2 = [8, 3, -8, 1];

alert( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25

此外,展开语法还可以用于合并数组

let arr = [3, 5, 1];
let arr2 = [8, 9, 15];

let merged = [0, ...arr, 2, ...arr2];

alert(merged); // 0,3,5,1,2,8,9,15 (0, then arr, then 2, then arr2)

在上面的示例中,我们使用了一个数组来演示展开语法,但任何可迭代对象都可以使用。

例如,这里我们使用展开语法将字符串转换为字符数组

let str = "Hello";

alert( [...str] ); // H,e,l,l,o

展开语法在内部使用迭代器来收集元素,与 for..of 的方式相同。

因此,对于字符串,for..of 返回字符,...str 变成 "H","e","l","l","o"。字符列表传递给数组初始化器 [...str]

对于此特定任务,我们还可以使用 Array.from,因为它将可迭代对象(如字符串)转换为数组

let str = "Hello";

// Array.from converts an iterable into an array
alert( Array.from(str) ); // H,e,l,l,o

结果与 [...str] 相同。

但是 Array.from(obj)[...obj] 之间存在细微差别

  • Array.from 同时适用于类数组和可迭代对象。
  • 展开语法仅适用于可迭代对象。

因此,对于将某些内容转换为数组的任务,Array.from 往往更通用。

复制数组/对象

还记得我们过去讨论过的 Object.assign()

使用展开语法可以执行相同操作。

let arr = [1, 2, 3];

let arrCopy = [...arr]; // spread the array into a list of parameters
                        // then put the result into a new array

// do the arrays have the same contents?
alert(JSON.stringify(arr) === JSON.stringify(arrCopy)); // true

// are the arrays equal?
alert(arr === arrCopy); // false (not same reference)

// modifying our initial array does not modify the copy:
arr.push(4);
alert(arr); // 1, 2, 3, 4
alert(arrCopy); // 1, 2, 3

请注意,可以执行相同操作来复制对象

let obj = { a: 1, b: 2, c: 3 };

let objCopy = { ...obj }; // spread the object into a list of parameters
                          // then return the result in a new object

// do the objects have the same contents?
alert(JSON.stringify(obj) === JSON.stringify(objCopy)); // true

// are the objects equal?
alert(obj === objCopy); // false (not same reference)

// modifying our initial object does not modify the copy:
obj.d = 4;
alert(JSON.stringify(obj)); // {"a":1,"b":2,"c":3,"d":4}
alert(JSON.stringify(objCopy)); // {"a":1,"b":2,"c":3}

这种复制对象的方式比 let objCopy = Object.assign({}, obj) 或数组 let arrCopy = Object.assign([], arr) 短得多,因此我们希望尽可能使用它。

总结

当我们在代码中看到 "..." 时,它要么是剩余参数,要么是展开语法。

有一种简单的方法可以区分它们

  • ... 位于函数参数的末尾时,它就是“剩余参数”,并将参数列表的其余部分收集到数组中。
  • ... 出现在函数调用或类似位置时,它被称为“展开语法”,并将数组展开为列表。

使用模式

  • 剩余参数用于创建接受任意数量参数的函数。
  • 展开语法用于将数组传递给通常需要多个参数列表的函数。

它们共同帮助轻松地在列表和参数数组之间进行转换。

函数调用的所有参数也存在于“旧式”arguments:类数组可迭代对象中。

教程地图

评论

在评论之前先阅读此内容…
  • 如果您有改进建议,请提交 GitHub 问题或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入一些代码,请使用 <code> 标记,对于多行代码,请用 <pre> 标记将其包装起来,对于超过 10 行的代码,请使用沙盒 (plnkrjsbincodepen…)