ES6的Spread语法

33

考虑以下示例代码

var x = ["a", "b", "c"];
var z = ["p", "q"];

var d = [...x, ...z];

var e = x.concat(z);

在这里,de的值完全相同,并且等于["a", "b", "c", "p", "q"],因此:

  1. 这两者之间到底有什么区别?
  2. 哪一个更有效率,为什么?
  3. 扩展语法的用途是什么?

虽然对于这个问题的答案:“你不认为在正式的广泛语言中引入这些小快捷方式可能会留下一些未被注意到的错误吗?”将基于观点,但我的观点是,是的,ES6的大部分内容将生成大量有缺陷的代码,因为粗心和/或初级开发人员将无法完全理解他们正在做什么。 - rockerest
嗯,它的主要用途当然不是连接。它是语法糖,但是是一个很好的语法糖。https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Spread_operator - Ahmet Cetin
@towerofnix那么我们为什么需要展开运算符? - void
1
@void 主要用于函数调用,例如如果 myFunc 接受未知数量的参数,我们可以使用展开运算符将参数作为数组传递。像这样:myFunc(...dynamicallyGeneratedArgs) - Nebula
1
如果您想将 z 添加到 x 而不创建另一个数组,则可以通过 x.push(...z); 实现真正的收益。 - Luca Rainone
显示剩余7条评论
7个回答

30
  1. 在您给出的示例中,两者实际上没有区别。
  2. .concat 明显更高效:http://jsperf.com/spread-into-array-vs-concat,因为...(spread)仅仅是一种基于更基本底层语法的语法糖,该语法明确迭代索引以扩展数组。
  3. 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,还适用于MapSet等其他对象。

这是一个很好的观点,并增加了这样一个想法:虽然在ES5中实现这一功能并非不可能,但展开运算符引入的功能是新语法中最有用的项目之一。


在这种特定的上下文中,展开运算符的实际底层语法(因为...也可以是“rest”参数),请参阅规范。如我上面所写的,“更基本的底层语法明确地迭代索引以展开数组”已足以表达要点,但实际定义使用GetValueGetIterator来处理其后跟的变量。

真实的使用示例:$.when 不允许将承诺数组作为参数,因此 $.when(...args) 很酷 :) - Grundy
2
Spread 在任意可迭代对象上都可以使用,这意味着它不仅适用于 Array,还适用于 MapSet 等其他对象。 - loganfsmyth
5
自Chrome 67版本(2018年5月29日)以来, Spread语法的速度(至少是两倍)比concat更快。 - Azteca
otherFunc.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

8

忽略问题的顺序,让我们从最基本的问题开始:什么是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' }

Rest参数语法与展开语法不同: Rest参数 语法看起来和展开语法一样,但实际上将未知数量的函数参数表示为数组。因此,与其"解包"可迭代对象不同,rest参数实际上将多个参数打包成一个数组。

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]

您可以参考此博客了解剩余/扩展运算符 - https://tejassavaliya.medium.com/es6-use-of-spread-rest-operator-in-javascript-f13b061b522f - Tejas Savaliya

4
这个例子的输出结果相同,但在内部处理上表现不同。
请考虑(检查浏览器的控制台):
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.iteratorSymbol.isConcatSpreadable

Spread 运算符使用 @@iterator 符号遍历数组和类数组对象,如:

  • Array.prototype
  • TypedArray.prototype
  • String.prototype
  • Map.prototype
  • Set.prototype

(这就是为什么你可以在它们上面使用 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" ]

1
在给定的示例中,这两者没有区别。对于连接,我们可以使用concat方法覆盖展开运算符。但是,展开运算符的使用不仅限于数组的连接。
展开语法允许扩展可迭代项,例如数组表达式或字符串。它可以用于以下情况。
1. 数组的展开运算符 - 数组连接 - 字符串转换为数组 - 将数组作为函数参数。
2. 对象的展开运算符 - 对象连接
要查看所有这些用途的演示并尝试编写代码,请按照下面的链接(codepen.io)。
ES6展开运算符演示:ES6-Demonstration of Spread Operator
/**
* 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};

0
背景:你想要连接两个数组,以便使用三个点扩展语法获得一个“按值”复制,但是你正在处理复杂/嵌套的数组。

发现:注意嵌套数组不是按值传递,而是按引用传递。换句话说,只有第一层项作为“按值”传递的副本。请参见以下示例:

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")


0

const colours = ['蓝色', '红色', '黑色']; // 简单数组。

const my_colours = ['蓝色', '红色', '黑色', '黄色', '绿色'];

const favourite_colours = [...my_colours, '灰色']; // [...] 扩展运算符可以访问另一个数组中的数据。


0

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开发者网站上更详细的文档。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接