展开语法和剩余参数语法

展开语法

展开语法(Spread syntax), 可以在函数调用/数组构造时, 将数组表达式或者string在语法层面展开;还可以在构造字面量对象时, 将对象表达式按key-value的方式展开。

语法

函数调用:

1
myFunction(...iterableObj);

字面量数组构造或字符串:

1
[...iterableObj, '4', ...'hello', 6];

构造字面量对象时,进行克隆或者属性拷贝(ECMAScript 2018规范新增特性):

1
let objClone = { ...obj };

应用场景

函数调用中使用

等价于apply方式

如果想将数组元素迭代为函数参数,一般使用Function.prototype.apply 的方式进行调用。

1
2
3
function myFunction(x, y, z) { }
var args = [0, 1, 2];
myFunction.apply(null, args);

有了展开语法,可以这样写:

1
2
3
function myFunction(x, y, z) { }
var args = [0, 1, 2];
myFunction(...args);

所有参数都可以通过展开语法来传值,也不限制多次使用展开语法。

1
2
3
function myFunction(v, w, x, y, z) { }
var args = [0, 1];
myFunction(-1, ...args, 2, ...[3]);

构造字面量数组时使用

数组合并

Array.concat函数常用于将一个数组连接到另一个数组的后面。如果不使用展开语法, 代码可能是下面这样的:

1
2
3
4
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
// 将 arr2 中所有元素附加到 arr1 后面并返回
var arr3 = arr1.concat(arr2);

使用展开语法:

1
2
3
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
var arr3 = [...arr1, ...arr2];

Array.unshift 方法常用于在数组的开头插入新元素/数组. 不使用展开语法, 示例如下:

1
2
3
4
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
// 将 arr2 中的元素插入到 arr1 的开头
Array.prototype.unshift.apply(arr1, arr2) // arr1 现在是 [3, 4, 5, 0, 1, 2]

如果使用展开语法, 代码如下: [请注意, 这里使用展开语法创建了一个新的 arr1 数组, Array.unshift 方法则是修改了原本存在的 arr1 数组]:

1
2
3
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
arr1 = [...arr2, ...arr1]; // arr1 现在为 [3, 4, 5, 0, 1, 2]

如果你想要整合两个数组,并且想把某个数组放在另一个数组的任意特定位置上,你可以这么做:

1
2
3
4
var arr1 = ['two', 'three'];
var arr2 = ['one', ...arr1, 'four', 'five'];

// ["one", "two", "three", "four", "five"]

这是一种比其他方式更短的语句!

数组拷贝

得到一份数组的拷贝是很常见的任务,过去我们使用Array.prototype.slice来做,但现在我们可以使用展开运算符:

1
2
3
var arr = [1,2,3];
var arr2 = [...arr]; // 和arr.slice()差不多
arr2.push(4)

记住:数组中的对象依然是引用值,所以不是任何东西都“拷贝”过去了。

提示: 实际上, 展开语法和 Object.assign() 行为一致, 执行的都是浅拷贝(只遍历一层)。如果想对多维数组进行深拷贝, 下面的示例就有些问题了。

1
2
3
4
var a = [[1], [2], [3]];
var b = [...a];
b.shift().shift(); // 1
// Now array a is affected as well: [[], [2], [3]]

将类数组转换为数组

像拷贝数组一样,我们常常使用Array.Prototype.slice来将NodeListarguments这种类数组对象转换为真正的数组。但是现在我们能够用展开运算符轻易的实现这项任务:

1
[...document.querySelectorAll('div')]

使用此法,你甚至可以像数组一样获取参数。

1
2
3
var myFn = function(...args) {
// ...
}

别忘了也能用Array.from达成相同的目的!

构造字面量对象时使用

可将已有对象的所有可枚举(enumerable)属性拷贝到新构造的对象中.

浅拷贝(Shallow-cloning, 不包含 prototype) 和对象合并, 可以使用更简短的展开语法。而不必再使用 Object.assign() 方式.

1
2
3
4
5
6
7
8
var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };

var clonedObj = { ...obj1 };
// 克隆后的对象: { foo: "bar", x: 42 }

var mergedObj = { ...obj1, ...obj2 };
// 合并后的对象: { foo: "baz", x: 42, y: 13 }

提示: Object.assign() 函数会触发 setters,而展开语法则不会。

展开运算符使用时需注意

  • 其只能用于可迭代对象

    在数组或函数参数中使用展开语法时, 该语法只能用于 可迭代对象

    1
    2
    var obj = {'key1': 'value1'};
    var array = [...obj]; // TypeError: obj is not iterable
  • 在函数调用时使用展开语法,请注意不能超过 JavaScript 引擎限制的最大参数个数。更多详细信息,请参考: apply()

剩余参数语法

剩余参数语法允许我们将一个不定数量的参数表示为一个数组。

语法

1
2
3
function(a, b, ...theArgs) {
// ...
}

此处theArgs将收集该函数的第三个参数(因为第一个参数被映射到a,而第二个参数映射到b)和所有后续参数。

剩余参数和 arguments对象的区别

剩余参数和 arguments对象之间的区别主要有三个:

  • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参。
  • arguments对象不是一个真正的数组,而剩余参数是真正的 Array实例,也就是说你能够在它上面直接使用所有的数组方法,比如 sortmapforEachpop
  • arguments对象还有一些附加的属性 (如callee属性)。

因为theArgs是个数组,所以你可以使用length属性得到剩余参数的个数,你可以在剩余参数上使用任意的数组方法,而arguments对象不可以。

从arguments到数组

引入了剩余参数来减少由参数引起的样板代码。

1
2
3
4
5
6
7
8
9
10
11
12
// Before rest parameters, the following could be found:
function f(a, b) {
var args = Array.prototype.slice.call(arguments, f.length);

// …
}

// to be equivalent of

function f(a, b, ...args) {

}

解构剩余参数

剩余参数可以被解构,这意味着他们的数据可以被解包到不同的变量中。请参阅解构赋值

1
2
3
4
5
6
7
function f(...[a, b, c]) {
return a + b + c;
}

f(1) // NaN (b and c are undefined)
f(1, 2, 3) // 6
f(1, 2, 3, 4) // 6 (the fourth parameter is not destructured)

展开语法和剩余参数语法比较

剩余语法(Rest syntax) 看起来和展开语法完全相同,不同点在于, 剩余参数用于解构数组和对象。从某种意义上说,剩余语法与展开语法是相反的:展开语法将数组展开为其中的各个元素,而剩余语法则是将多个元素收集起来并“凝聚”为单个元素。

参考资源:

展开语法-mdn

剩余参数-mdn

es6展开运算符的6个妙用

坚持原创技术分享,您的支持将鼓励我继续创作!