一、在 JavaScript 中,很多内建函数都支持传入任意数量的参数。

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

二、当我们在代码中看到”…”时,它要么是 rest 参数,要么就是 spread 语法。
1、有一个简单的方法可以区分它们:

  • 若…出现在函数参数列表的最后,那么它就是 rest 参数,它会把参数列表中剩余的参数收集到一个数组中。
  • 若…出现在函数调用或类似的表达式中,那它就是 spread 语法,它会把一个数组展开为列表。

2、使用场景:

  • Rest 参数用于创建可接受任意数量参数的函数。
  • Spread 语法用于将数组传递给通常需要含有许多参数的列表的函数。

3、它们俩的出现帮助我们轻松地在列表和参数数组之间来回转换。
三、“旧式”的arguments(类数组且可迭代的对象)也依然能够帮助我们获取函数调用中的所有参数。

Rest 参数…

一、在 JavaScript 中,无论函数是如何定义的,你都可以使用任意数量的参数调用函数。

  1. function sum(a, b) {
  2. return a + b;
  3. }
  4. alert( sum(1, 2, 3, 4, 5) );

1、虽然这里不会因为传入“过多”的参数而报错。但是当然,在结果中只有前两个参数被计算进去了。
二、Rest 参数可以通过使用三个点…并在后面跟着包含剩余参数的数组名称,来将它们包含在函数定义中。这些点的字面意思是“将剩余参数收集到一个数组中”。
【示例1】我们需要把所有的参数都放到数组args中:

  1. function sumAll(...args) { // 数组名为 args
  2. let sum = 0;
  3. for (let arg of args) sum += arg;
  4. return sum;
  5. }
  6. alert( sumAll(1) ); // 1
  7. alert( sumAll(1, 2) ); // 3
  8. alert( sumAll(1, 2, 3) ); // 6

三、我们也可以选择获取第一个参数作为变量,并将剩余的参数收集起来。
【示例1】把前两个参数定义为变量,并把剩余的参数收集到titles数组中:

  1. function showName(firstName, lastName, ...titles) {
  2. alert( firstName + ' ' + lastName ); // Julius Caesar
  3. // 剩余的参数被放入 titles 数组中
  4. // i.e. titles = ["Consul", "Imperator"]
  5. alert( titles[0] ); // Consul
  6. alert( titles[1] ); // Imperator
  7. alert( titles.length ); // 2
  8. }
  9. showName("Julius", "Caesar", "Consul", "Imperator");

四、Rest 参数必须放到参数列表的末尾
【示例1】Rest 参数会收集剩余的所有参数,因此下面这种用法没有意义,并且会导致错误:

  1. function f(arg1, ...rest, arg2) { // arg2 在 ...rest 后面?!
  2. // error // rest必须处在最后
  3. }

“arguments” 变量

一、有一个名为arguments的特殊的类数组对象,该对象按参数索引包含所有参数。
【示例1】

  1. function showName() {
  2. alert( arguments.length );
  3. alert( arguments[0] );
  4. alert( arguments[1] );
  5. // 它是可遍历的
  6. // for(let arg of arguments) alert(arg);
  7. }
  8. // 依次显示:2,Julius,Caesar
  9. showName("Julius", "Caesar");
  10. // 依次显示:1,Ilya,undefined(没有第二个参数)
  11. showName("Ilya");

二、在过去,JavaScript 中没有 rest 参数,而使用arguments是获取函数所有参数的唯一方法。现在它仍然有效,我们可以在一些老代码里找到它。
三、arguments缺点
1、尽管arguments是一个类数组,也是可迭代对象,但它终究不是数组。它不支持数组方法,因此我们不能调用arguments.map(…)等方法。
2、它始终包含所有参数,我们不能像使用 rest 参数那样只截取入参的一部分。
四、因此,当我们需要这些功能时,最好使用 rest 参数。
五、箭头函数是没有”arguments”
【示例1】如果我们在箭头函数中访问arguments,访问到的arguments并不属于箭头函数,而是属于箭头函数外部的“普通”函数。

  1. function f() {
  2. let showArg = () => alert(arguments[0]);
  3. showArg();
  4. }
  5. f(1); // 1

Spread 语法

一、内建函数Math.max会返回参数中最大的值:

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

1、假如我们有一个数组[3, 5, 1],我们该如何用它调用Math.max呢?
(1)直接把数组“原样”传入是不会奏效的,因为Math.max希望你传入一个列表形式的数值型参数,而不是一个数组:

  1. let arr = [3, 5, 1];
  2. alert( Math.max(arr) ); // NaN

2、毫无疑问,我们不可能手动地去一一设置参数Math.max(arg[0], arg[1], arg[2]),因为我们不确定这儿有多少个。在脚本执行时,可能参数数组中有很多个元素,也可能一个都没有。并且这样设置的代码也很丑。
3、这是可以用Spread 语法
二、Spread看起来和 rest 参数很像,也使用…,但是二者的用途完全相反。
1、当在函数调用中使用…arr时,它会把可迭代对象arr“展开”到参数列表中。
【示例1】以Math.max为例:

  1. let arr = [3, 5, 1];
  2. alert( Math.max(...arr) ); // 5(spread 语法把数组转换为参数列表)

【示例2】我们还可以通过这种方式传递多个可迭代对象:

  1. let arr1 = [1, -2, 3, 4];
  2. let arr2 = [8, 3, -8, 1];
  3. alert( Math.max(...arr1, ...arr2) ); // 8

【示例3】我们甚至还可以将 spread 语法与常规值结合使用:

  1. let arr1 = [1, -2, 3, 4];
  2. let arr2 = [8, 3, -8, 1];
  3. alert( Math.max(1, ...arr1, 2, ...arr2, 25) ); // 25

【示例4】并且,我们还可以使用 spread 语法来合并数组:

  1. let arr = [3, 5, 1];
  2. let arr2 = [8, 9, 15];
  3. let merged = [0, ...arr, 2, ...arr2];
  4. alert(merged); // 0,3,5,1,2,8,9,15(0,然后是 arr,然后是 2,然后是 arr2)

2、在上面的示例中,我们使用数组展示了 spread 语法,其实任何可迭代对象都可以。
【示例5】在这儿我们使用 spread 语法将字符串转换为字符数组:

  1. let str = "Hello";
  2. alert( [...str] ); // H,e,l,l,o

三、Spread 语法内部使用了迭代器来收集元素,与for..of的方式相同。
四、因此,对于一个字符串,for..of会逐个返回该字符串中的字符,…str也同理会得到”H”,”e”,”l”,”l”,”o”这样的结果。随后,字符列表被传递给数组初始化器[…str]。
五、对于这个特定任务,我们还可以使用Array.from来实现,因为该方法会将一个可迭代对象(如字符串)转换为数组:

  1. let str = "Hello";
  2. // Array.from 将可迭代对象转换为数组
  3. alert( Array.from(str) ); // H,e,l,l,o

1、运行结果与[…str]相同。
2、不过Array.from(obj)和[…obj]存在一个细微的差别:

  • Array.from适用于类数组对象也适用于可迭代对象。
  • Spread 语法只适用于可迭代对象。

3、因此,对于将一些“东西”转换为数组的任务,Array.from往往更通用。

获取一个 array/object 的副本

一、使用 spread 语法也可以做Object.assign()同样的事情(译注:也就是进行浅拷贝)。

  1. let arr = [1, 2, 3];
  2. let arrCopy = [...arr]; // 将数组 spread 到参数列表中
  3. // 然后将结果放到一个新数组
  4. // 两个数组中的内容相同吗?
  5. alert(JSON.stringify(arr) === JSON.stringify(arrCopy)); // true
  6. // 两个数组相等吗?
  7. alert(arr === arrCopy); // false(它们的引用是不同的)
  8. // 修改我们初始的数组不会修改副本:
  9. arr.push(4);
  10. alert(arr); // 1, 2, 3, 4
  11. alert(arrCopy); // 1, 2, 3

二、也可以通过相同的方式来复制一个对象:

  1. let obj = { a: 1, b: 2, c: 3 };
  2. let objCopy = { ...obj }; // 将对象 spread 到参数列表中
  3. // 然后将结果返回到一个新对象
  4. // 两个对象中的内容相同吗?
  5. alert(JSON.stringify(obj) === JSON.stringify(objCopy)); // true
  6. // 两个对象相等吗?
  7. alert(obj === objCopy); // false (not same reference)
  8. // 修改我们初始的对象不会修改副本:
  9. obj.d = 4;
  10. alert(JSON.stringify(obj)); // {"a":1,"b":2,"c":3,"d":4}
  11. alert(JSON.stringify(objCopy)); // {"a":1,"b":2,"c":3}

三、这种方式比使用let arrCopy = Object.assign([], arr);来复制数组,或使用let objCopy = Object.assign({}, obj);来复制对象写起来要短得多。因此,只要情况允许,我们更喜欢使用它。