扩展运算符与 array.concat()

Posted

技术标签:

【中文标题】扩展运算符与 array.concat()【英文标题】:spread operator vs array.concat() 【发布时间】:2018-07-29 15:52:09 【问题描述】:

spread operatorarray.concat()有什么区别

let parts = ['four', 'five'];
let numbers = ['one', 'two', 'three'];
console.log([...numbers, ...parts]);

Array.concat()函数

let parts = ['four', 'five'];
let numbers = ['one', 'two', 'three'];
console.log(numbers.concat(parts));

两个结果是一样的。那么,我们想在什么样的场景下使用它们呢?哪一个最适合性能?

【问题讨论】:

@gurvinder372 我认为 OP 知道这一点。 measurethat.net/Benchmarks/Show/579/1/… @FabienGreard 这是很好的信息。 可能重复***.com/questions/39716216/… @nicowernli 我问的是 concat() 而不是 push()。 【参考方案1】:

console.log(['one', 'two', 'three', 'four', 'five']) 也有相同的结果,那么为什么在这里使用任何一个呢? :P

一般来说,当您有两个(或更多)来自任意来源的数组时,您会使用concat,如果之前知道始终是数组一部分的其他元素,您将在数组文字中使用展开语法。因此,如果您的代码中有一个带有 concat 的数组字面量,则只需使用扩展语法,否则只需使用 concat

[...a, ...b] // bad :-(
a.concat(b) // good :-)

[x, y].concat(a) // bad :-(
[x, y, ...a]    // good :-)

在处理非数组值时,这两种选择的行为也完全不同。

【讨论】:

是否可以在另一个数组之间连接或传播一个数组? @RameshRajendran 在另外两个阵列之间,当然。 “在 an 其他数组之间”,不确定您的意思。 @RameshRajendran 等价于['one'].concat(parts, ['two', 'three'])(如果您不想传递多个参数,则为['one'].concat(parts).concat(['two', 'three']) FWIW,存在可衡量的性能差异。见jsperf.com/spread-vs-concat-vs-push @DrazenBjelovuk .concat(x) 让读者假设x 也是一个数组。当然,concat 也可以处理非数组值,但 imo 这不是它的主要操作模式。特别是如果x 是任意(未知)值,您需要编写.concat([x]) 以确保它始终按预期工作。无论如何,一旦您必须编写数组文字,我说您应该只使用扩展语法而不是concat【参考方案2】:

我认为有效的一个区别是,对大数组使用扩展运算符会给您带来Maximum call stack size exceeded 的错误,您可以避免使用concat 运算符。

var  someArray = new Array(600000);
var newArray = [];
var tempArray = [];


someArray.fill("foo");

try 
  newArray.push(...someArray);
 catch (e) 
  console.log("Using spread operator:", e.message)


tempArray = newArray.concat(someArray);
console.log("Using concat function:", tempArray.length)

【讨论】:

这种使用扩展语法(函数调用)并不是我们所要求的(数组字面量)。 我知道,OP 没有push 元素。但我们通常 push 数组中的元素,所以我试图展示我们将推送与传播一起使用时的后果。 您应该澄清,当函数调用在内部使用扩展时,堆栈会被使用。然而,当它只是一个数组字面量并且使用传播时,不会使用堆栈,因此不会发生最大调用堆栈。 这与问题及其误导性无关【参考方案3】:

concat 和价差在参数不是数组时非常不同。

当参数不是数组时,concat 将它作为一个整体添加,而... 尝试迭代它,如果不能迭代则失败。考虑:

a = [1, 2, 3]
x = 'hello';

console.log(a.concat(x));  // [ 1, 2, 3, 'hello' ]
console.log([...a, ...x]); // [ 1, 2, 3, 'h', 'e', 'l', 'l', 'o' ]

这里,concat 以原子方式处理字符串,而... 使用其默认迭代器,逐字符。

另一个例子:

x = 99;

console.log(a.concat(x));   // [1, 2, 3, 99]
console.log([...a, ...x]);  // TypeError: x is not iterable

同样,对于concat,数字是一个原子,... 尝试迭代它并失败。

最后:

function* gen()  yield *'abc' 

console.log(a.concat(gen()));   // [ 1, 2, 3, Object [Generator]  ]
console.log([...a, ...gen()]);  // [ 1, 2, 3, 'a', 'b', 'c' ]

concat 不尝试迭代生成器并将其作为一个整体附加,而... 很好地从中获取所有值。

总而言之,当您的参数可能是非数组时,concat... 之间的选择取决于您是否希望它们被迭代。

上面描述了concat的默认行为,但是,ES6 provides a way用Symbol.isConcatSpreadable覆盖它。默认情况下,此符号为 true 用于数组,false 用于其他所有内容。将其设置为 true 告诉 concat 迭代参数,就像 ... 所做的那样:

str = 'hello'
console.log([1,2,3].concat(str)) // [1,2,3, 'hello']

str = new String('hello');
str[Symbol.isConcatSpreadable] = true;
console.log([1,2,3].concat(str)) // [ 1, 2, 3, 'h', 'e', 'l', 'l', 'o' ]

性能方面concat 更快,可能是因为它可以受益于特定于数组的优化,而... 必须符合通用迭代协议。时间:

let big = (new Array(1e5)).fill(99);
let i, x;

console.time('concat-big');
for(i = 0; i < 1e2; i++) x = [].concat(big)
console.timeEnd('concat-big');

console.time('spread-big');
for(i = 0; i < 1e2; i++) x = [...big]
console.timeEnd('spread-big');


let a = (new Array(1e3)).fill(99);
let b = (new Array(1e3)).fill(99);
let c = (new Array(1e3)).fill(99);
let d = (new Array(1e3)).fill(99);

console.time('concat-many');
for(i = 0; i < 1e2; i++) x = [1,2,3].concat(a, b, c, d)
console.timeEnd('concat-many');

console.time('spread-many');
for(i = 0; i < 1e2; i++) x = [1,2,3, ...a, ...b, ...c, ...d]
console.timeEnd('spread-many');

【讨论】:

这应该是正确的答案。比 bergi 的回答更主观。【参考方案4】:

我只是回答性能问题,因为已经有关于场景的很好的答案。我写了一个测试并在最新的浏览器上执行它。下面是结果和代码。

/*
 * Performance results.
 * Browser           Spread syntax      concat method
 * --------------------------------------------------
 * Chrome 75         626.43ms           235.13ms
 * Firefox 68        928.40ms           821.30ms
 * Safari 12         165.44ms           152.04ms
 * Edge 18           1784.72ms          703.41ms
 * Opera 62          590.10ms           213.45ms
 * --------------------------------------------------
*/

下面是我编写和使用的代码。

const array1 = [];
const array2 = [];
const mergeCount = 50;
let spreadTime = 0;
let concatTime = 0;

// Used to popolate the arrays to merge with 10.000.000 elements.
for (let i = 0; i < 10000000; ++i) 
    array1.push(i);
    array2.push(i);


// The spread syntax performance test.
for (let i = 0; i < mergeCount; ++i) 
    const startTime = performance.now();
    const array3 = [ ...array1, ...array2 ];

    spreadTime += performance.now() - startTime;


// The concat performance test.
for (let i = 0; i < mergeCount; ++i) 
    const startTime = performance.now();
    const array3 = array1.concat(array2);

    concatTime += performance.now() - startTime;


console.log(spreadTime / mergeCount);
console.log(concatTime / mergeCount);

【讨论】:

谢谢,就实际问题而言,这实际上是一个有用的答案。 我很少有 10.000.000 个元素。宁愿/也希望看到合并 10、100 或 1000 个元素并进行多次合并的比较。【参考方案5】:

虽然有些回复在大数组的性能方面是正确的,但在处理小数组时性能却大不相同。

您可以通过https://jsperf.com/spread-vs-concat-size-agnostic自己查看结果。

如您所见,spread 在较小的阵列上速度提高了 50%,而concat 在大型阵列上速度提高了数倍。

【讨论】:

链接已损坏——因此,最好提供链接内容的摘要,以防链接损坏。【参考方案6】:

concatpush 之间有一个非常重要的区别,前者不会改变底层数组,要求您将结果分配给相同或不同的数组:

let things = ['a', 'b', 'c'];
let moreThings = ['d', 'e'];
things.concat(moreThings);
console.log(things); // [ 'a', 'b', 'c' ]
things.push(...moreThings);
console.log(things); // [ 'a', 'b', 'c', 'd', 'e' ]

我看到了由concat 更改数组的假设引起的错误(替朋友说话;)。

【讨论】:

这是正确的答案,谢谢你,保罗。

以上是关于扩展运算符与 array.concat()的主要内容,如果未能解决你的问题,请参考以下文章

扩展运算符与rest参数

不同行为的三元运算符 - if/else 与扩展运算符 (...)

rest 参数与扩展运算符

ES6-11学习笔记--扩展运算符与rest参数

在类方法与函数中使用扩展运算符

扩展运算符与rest参数