考虑以下示例代码
var x = ["a", "b", "c"];
var z = ["p", "q"];
var d = [...x, ...z];
var e = x.concat(z);
在这里,d
和e
的值完全相同,并且等于["a", "b", "c", "p", "q"]
,因此:
- 这两者之间到底有什么区别?
- 哪一个更有效率,为什么?
- 扩展语法的用途是什么?
考虑以下示例代码
var x = ["a", "b", "c"];
var z = ["p", "q"];
var d = [...x, ...z];
var e = x.concat(z);
在这里,d
和e
的值完全相同,并且等于["a", "b", "c", "p", "q"]
,因此:
.concat
明显更高效:http://jsperf.com/spread-into-array-vs-concat,因为...
(spread)仅仅是一种基于更基本底层语法的语法糖,该语法明确迭代索引以扩展数组。关于上面第3点的详细说明,您对spread的使用是一个有些牵强的例子(尽管这种情况可能经常出现)。例如,当整个参数列表应该在函数体内传递给.call
时,Spread就非常有用。
function myFunc(){
otherFunc.call( myObj, ...args );
}
对比
function myFunc(){
otherFunc.call( myObj, args[0], args[1], args[2], args[3], args[4] );
}
这是另一个任意的例子,但是在某些原本冗长且笨拙的情况下使用展开运算符会更加方便易用。
正如@loganfsmyth所指出的:
展开运算符也适用于任意可迭代对象,这意味着它不仅适用于
Array
,还适用于Map
和Set
等其他对象。
这是一个很好的观点,并增加了这样一个想法:虽然在ES5中实现这一功能并非不可能,但展开运算符引入的功能是新语法中最有用的项目之一。
...
也可以是“rest”参数),请参阅规范。如我上面所写的,“更基本的底层语法明确地迭代索引以展开数组”已足以表达要点,但实际定义使用GetValue
和GetIterator
来处理其后跟的变量。$.when
不允许将承诺数组作为参数,因此 $.when(...args)
很酷 :) - GrundyArray
,还适用于 Map
和 Set
等其他对象。 - loganfsmythconcat
更快。 - AztecaotherFunc.call( myObj, args[0], args[1], args[2], args[3], args[4] );
这似乎是一个非常糟糕的例子。它不仅是人为制造的,而且误导性很强。几乎所有编写 ES6 之前代码的人都会使用 otherFunc.apply( myObj, args );
,这基本上具有相同的语义,而且没有失去清晰度。当 this
不重要时,好的做法是 func( ...args )
而不是更冗长和不必要的 func.apply( null, args )
。 - VLAZ忽略问题的顺序,让我们从最基本的问题开始:什么是Spread语法的用途?
Spread语法基本上是展开可迭代对象(例如数组或对象)的元素。或者,更详细的解释可以查看MDN Web Docs关于Spread语法的文档:
Spread语法允许在期望零个或多个参数的位置(对于函数调用)或元素(对于数组字面量),或在期望零个或多个键值对的位置(对于对象字面量)中展开可迭代对象(例如数组表达式或字符串)或对象表达式。
以下是一些Spread语法的典型用例示例以及Spread语法和剩余参数之间差异的示例(它们可能看起来相同,但执行的功能几乎相反)。
函数调用:
const multiArgs = (one, two) => {
console.log(one, two);
};
const args = [1, 2];
multiArgs(...args);
// 1 2
数组或字符串字面量:
const arr1 = [2, 3];
const arr2 = [1, ...arr1, 4];
console.log(arr2);
// [1, 2, 3, 4]
const s = 'split';
console.log(...s);
// s p l i t
对象字面量:
const obj1 = { 1: 'one' };
const obj2 = { 2: 'two' };
const obj3 = { ...obj1, ...obj2 };
console.log(obj3);
// { 1: 'one', 2: 'two' }
const multiArgs = (...args) => {
console.log(args);
};
multiArgs('a', 'b', 'c');
// ['a', 'b', 'c']
展开语法的性能/效率:
回答有关效率与其他方法的问题,唯一诚实的答案是“这取决于情况”。浏览器不断变化,特定函数相关的上下文和数据会导致极其不同的性能结果,因此您可以找到各种相互矛盾的性能计时,表明展开语法比您可能用来完成类似目标的各种数组或对象方法快得惊人或慢得荒谬。最终,在任何需要优化速度的情况下,都应该进行比较测试,而不是依赖于简单函数的通用计时,这些函数忽略了您代码和数据的具体内容。
与concat()
的比较:
concat()
之间的区别,需要做一个快速的说明。区别在于,展开语法不仅可以用于连接数组,还可以用于许多其他情况,但是 concat()
可以在旧版浏览器(如 IE)中使用。如果您不关心与旧版浏览器的兼容性,并且对速度进行微小优化是不必要的情况下,则展开语法和 concat()
之间的选择只是一种可读性的问题:arr3 = arr1.concat(arr2)
或者 arr3 = [...arr1, ...arr2]
。var x = [], y = [];
x[1] = "a";
y[1] = "b";
var usingSpread = [...x, ...y];
var usingConcat = x.concat(y);
console.log(usingSpread); // [ undefined, "a", undefined, "b"]
console.log(usingConcat); // [ , "a", , "b"]
console.log(1 in usingSpread); // true
console.log(1 in usingConcat); // false
Array.prototype.concat 方法会保留数组中的 空洞(empty slots),而 Spread 运算符会用 undefined
值替换它们。
介绍 Symbol.iterator 和 Symbol.isConcatSpreadable :
Spread 运算符使用 @@iterator
符号遍历数组和类数组对象,如:
(这就是为什么你可以在它们上面使用 for ... of
循环)
我们可以重写默认的 iterator
符号来查看 spread
运算符的行为:
var myIterable = ["a", "b", "c"];
var myIterable2 = ["d", "e", "f"];
myIterable[Symbol.iterator] = function*() {
yield 1;
yield 2;
yield 3;
};
console.log(myIterable[0], myIterable[1], myIterable[2]); // a b c
console.log([...myIterable]); // [1,2,3]
var result = [...myIterable, ...myIterable2];
console.log(result); // [1,2,3,"d","e","f"]
var result2 = myIterable.concat(myIterable2);
console.log(result2); // ["a", "b", "c", "d", "e", "f"]
@@isConcatSpreadable
是一个布尔值属性,如果为true,则表示应该通过Array.prototype.concat将对象扁平化为其数组元素。
If set to false
, Array.concat
will not flatten the array :
const alpha = ['a', 'b', 'c'];
const numeric = [1, 2, 3];
let alphaNumeric = alpha.concat(numeric);
// console.log(alphaNumeric);
numeric[Symbol.isConcatSpreadable] = false;
alphaNumeric = alpha.concat(numeric);
// alphaNumeric = [...alpha, ...numeric];
// the above line will output : ["a","b","c",1,2,3]
console.log(JSON.stringify(alphaNumeric)); // ["a","b","c",[1,2,3]]
然而,当涉及到对象
时,展开运算符
的 表现不同,因为它们是不可迭代的
var obj = {'key1': 'value1'};
var array = [...obj]; // TypeError: obj is not iterable
var objCopy = {...obj}; // copy
它从提供的对象上复制自己可枚举的属性到一个新对象上。
展开运算符更快,可以查看 spread-into-array-vs-concat(至少在Chrome 67中)。
还可以参考如何使用三个点改变JavaScript来了解一些用例,其中包括 解构赋值(数组或对象):
const arr = [1, 2, 3, 4, 5, 6, 7];
const [first, , third, ...rest] = arr;
console.log({ first, third, rest });
console.log( [...'hello'] ) // [ "h", "e" , "l" , "l", "o" ]
/**
* Example-1: Showing How Spread Operator can be used to concat two or more
arrays.
*/
const americas = ['South America', 'North America'];
const eurasia = ['Europe', 'Asia'];
const world = [...americas, ...eurasia];
/**
* Example-2: How Spread Operator can be used for string to array.
*/
const iLiveIn = 'Asia';
const iLiveIntoArray = [...iLiveIn];
/**
* Example-3: Using Spread Operator to pass arguments to function
*/
const numbers = [1,4,5];
const add = function(n1,n2,n3){
return n1 + n2 + n3;
};
const addition = add(numbers[0],numbers[1],numbers[2]);
const additionUsingSpread = add(...numbers);
/**
* Example-4: Spread Operator, can be used to concat the array
*/
const personalDetails = {
name: 'Ravi',
age: '28',
sex: 'male'
};
const professionalDetails = {
occupation: 'Software Engineer',
workExperience: '4 years'
};
const completeDetails = {...personalDetails, ...professionalDetails};
发现:注意嵌套数组不是按值传递,而是按引用传递。换句话说,只有第一层项作为“按值”传递的副本。请参见以下示例:
sourceArray1 = [ 1, [2, 3] ] // Third element is a nested array
sourceArray2 = [ 4, 5 ]
targetArray = [ ...sourceArray1, ...sourceArray2]
console.log("Target array result:\n", JSON.stringify(targetArray), "\n\n") //it seems a copy, but...
console.log("Let's update the first source value:\n")
sourceArray1[0] = 10
console.log("Updated source array:\n", JSON.stringify(sourceArray1), "\n")
console.log("Target array is NOT updated, It keeps a copy by value: 1\n")
console.log(JSON.stringify(targetArray), "\n\n")
//But if you update a nested value, it has NOT been copied
console.log("Let's update a nested source value:\n")
sourceArray1[1][0] = 20
console.log("Updated source nested array:\n", JSON.stringify(sourceArray1), "\n")
console.log("Target array is updated BY REFERENCE!\n")
console.log(JSON.stringify(targetArray)) // it is not a copy, it is a reference!
console.log("\nCONCLUSION: ... spread syntax make a copy 'by value' for first level elements, but 'by reference' for nested/complex elements (This applies also for objects) so take care!\n")
const colours = ['蓝色', '红色', '黑色']; // 简单数组。
const my_colours = ['蓝色', '红色', '黑色', '黄色', '绿色'];
const favourite_colours = [...my_colours, '灰色']; // [...] 扩展运算符可以访问另一个数组中的数据。
Spread语法允许在期望零个或多个元素的位置展开可迭代对象。这种高级解释可能会令人困惑,因此以下是一个“现实世界”的例子:
如果没有Spread语法,您可能需要多次更新对象,如下所示:
//If I needed to change the speed or damage at any time of a race car
const raceCar = {name: 'Ferrari 250 GT'};
const stats = {speed: 66, damage: 1, lap: 2};
raceCar['speed'] = stats.speed;
raceCar['damage'] = stats.damage;
或者,更简洁的解决方案是使用展开语法创建一个新对象:
//Creates a new object with priority from left to right
const lap1 = { ...raceCar, ...stats }
//Or a specific variable:
const enterPitStop = {...raceCar, speed: 0 }
本质上,与其改变raceCar的原始对象,你将创建一个新的不可变对象。
在向数组添加新值时,这也非常有用。使用spread操作符,您可以通过复制前面的数组来推入/推出多个变量。在spread操作符之前,您会像这样推入:
var raceCars = ['Ferrari 250 GT', 'Le Mans Series', '24 Heures du Mans'];
//Sometimes, you will need to push multiple items to an array, which gets messy in large projects!
raceCars.push('Car 1');
raceCars.push('Car 2');
raceCars.push('Car 3');
相反,您可以复制该数组并将其添加到新变量或同一变量中以简化操作。
//Push values to array
raceCars = [...raceCars, 'Car 1', 'Car 2', 'Car 3'];
//This is dynamic! Add the values anywhere in the array:
//Adds the values at the front as opposed to the end
raceCars = ['Car 1', 'Car 2', 'Car 3', ...raceCars];
//Another dynamic examples of adding not in the front or back:
raceCars = ['Car 1', 'Car 2', ...raceCars, 'Car 3'];
我鼓励您查看Mozilla开发者网站上更详细的文档。
myFunc
接受未知数量的参数,我们可以使用展开运算符将参数作为数组传递。像这样:myFunc(...dynamicallyGeneratedArgs)
。 - Nebulaz
添加到x
而不创建另一个数组,则可以通过x.push(...z);
实现真正的收益。 - Luca Rainone