展开运算符 vs 数组.concat()

137
< p>“spread operator”和“array.concat()”有什么区别?

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

Array.concat() function

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

两个结果是相同的。那么,我们想要在什么情况下使用它们?哪一个对性能最好?

2
@gurvinder372 我认为原帖的作者已经知道了。 - Ele
2
https://www.measurethat.net/Benchmarks/Show/579/1/arrayprototypeconcat-vs-spread-operator - Fabien Greard
@FabienGreard 这是不错的信息。 - Ramesh Rajendran
1
可能是重复问题 https://dev59.com/4VkS5IYBdhLWcg3wiXQj - nicowernli
1
@nicowernli 我在询问 concat() 而不是 push()。 - Ramesh Rajendran
显示剩余6条评论
8个回答

230

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
< p > 对于 concat ,数字是一个原子,... 尝试迭代它并失败了。 < /p> < p > 最后: < /p>
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 提供了一种方式 使用 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');


22
这应该是正确的答案。比Bergi的答案更客观。 - lintuxvi
1
由于某种原因,在我的TypeScript项目中,array.concat方法对我来说无法正常工作。所以我不得不使用array.push(...spread)方法。我找到了这个链接,它讨论了TypeScript中的concat问题,并建议使用([] as any[]).concat(myArray),但似乎也不起作用。 - Gangula

79

console.log(['one', 'two', 'three', 'four', 'five'])concat 的结果相同,那么在这里为什么要使用它们呢? :P

通常情况下,当你有来自任意来源的两个(或多个)数组时,你会使用 concat,而如果始终包含已知附加元素的数组文字,则会在数组文字中使用扩展语法。因此,如果你的代码中有一个带有 concat 的数组文字,只需使用扩展语法即可,否则就使用 concat

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

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

此外,当处理非数组值时,这两种替代方案的行为差异很大。


能否在另一个数组中间进行数组的合并或展开操作? - Ramesh Rajendran
例如,我可以在数组对象之间使用扩展运算符。 ['one',...parts,'two','three'];。现在 four and five 移动到第二个位置。这在 concat() 中可行吗? - Ramesh Rajendran
1
@RameshRajendran 相当于 ['one'].concat(parts, ['two', 'three'])(如果您不想传递多个参数,则为 ['one'].concat(parts).concat(['two', 'three']))。 - Bergi
10
就此而言,存在可衡量的性能差异。请参见https://jsperf.com/spread-vs-concat-vs-push。 - broofa
2
@DrazenBjelovuk .concat(x) 使读者认为 x 也是一个数组。当然,concat 也可以处理非数组值,但在我看来,这不是它的主要操作模式。特别是如果 x 是任意(未知)值,你需要编写 .concat([x]) 来确保它始终按预期工作。而且,一旦你必须编写一个数组字面量,我建议你使用扩展语法而不是 concat - Bergi
显示剩余9条评论

72

.concat() 更快,否则假设两个参数都是列表,则它们的工作方式相同。下面是合并每个具有 1000 万个元素的两个数组所需的时间(较低的值更好):

浏览器 [...a, ...b] a.concat(b)
Chrome 113 350ms 30ms
Firefox 113 400ms 63ms
Safari 16.4 92ms 71ms

我在一台配备 8GB RAM 的 M1 MacBook Air 上运行了此代码:

const arraySize = 10000000;
const trials = 50;

const array1 = [];
const array2 = [];
for (let i = 0; i < arraySize; ++i) {
  array1.push(i);
  array2.push(i);
}

let spreadTime = 0;
for (let i = 0; i < trials; ++i) {
  const start = performance.now();
  const array3 = [...array1, ...array2];
  const end = performance.now();

  spreadTime += end - start;
}

let concatTime = 0;
for (let i = 0; i < trials; ++i) {
  const start = performance.now();
  const array3 = array1.concat(array2);
  const end = performance.now();

  concatTime += end - start;
}

// performance.now() returns milliseconds with a
// 5 microsecond resolution in isolated contexts and a
// 100 microsecond resolution in non-isolated contexts.
spreadTime = Math.round(spreadTime / trials * 1000) / 1000;
concatTime = Math.round(concatTime / trials * 1000) / 1000;
console.log(`${arraySize} items - spread: ${spreadTime}ms concat: ${concatTime}ms`);


17
谢谢,实际上这是一个很有用的答案,涉及到真正重要的内容。 - King Friday
14
我很少有1000万个元素。更希望/也想看一下合并10、100或1000个元素,并多次执行合并的比较。 - daniero
1
我刚试了一个简单的脚本,发现使用spread更快。请参见https://stackoverflow.com/a/74161832/3370568 - Wajahath

14

我认为唯一有效的区别是,当使用展开运算符处理大型数组时,可能会出现错误: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)


2
这种使用扩展语法(函数调用)与所要求的不同(数组字面量)。 - Bergi
我知道,OP没有将元素“push”进去。但是我们通常会在数组中“push”元素,所以我正在尝试展示当我们使用spread和push时的后果。 - Ankit Agarwal
6
需要翻译的内容:You should clarify that the stack gets used when a function call uses spread inside. However when it is an array literal only and the spread is used, no stack is ever used so no max call stack will happen.您应该澄清,当函数调用内部使用spread时,堆栈将被使用。然而,当仅为数组文字并使用spread时,不会使用任何堆栈,因此不会发生最大调用堆栈。 - Luis Villavicencio
2
这与问题无关,有点误导性。 - bumbeishvili

9

更新:

concat现在始终比spread更快。以下基准测试显示了合并小型和大型数组的性能差异: https://jsbench.me/nyla6xchf4/1

enter image description here

// preparation
const a = Array.from({length: 1000}).map((_, i)=>`${i}`);
const b = Array.from({length: 2000}).map((_, i)=>`${i}`);
const aSmall = ['a', 'b', 'c', 'd'];
const bSmall = ['e', 'f', 'g', 'h', 'i'];

const c = [...a, ...b];
// vs
const c = a.concat(b);

const c = [...aSmall, ...bSmall];
// vs
const c = aSmall.concat(bSmall)

之前:

虽然在大数组上,一些回答是正确的,但在处理小数组时,性能相当不同。

您可以在https://jsperf.com/spread-vs-concat-size-agnostic上自行检查结果。

正如您所看到的,展开操作符对于较小的数组要快50%,而concat在大数组上快多次。


5
链接已经失效,因此最好提供链接内容的摘要,以防链接失效。 - Scott Schupbach
不确定这是否正确。请参见https://stackoverflow.com/a/74161832/3370568 - Wajahath
1
由于浏览器的改变,它已经不完全正确了,但是链接的答案也不正确。这两个测试都包括准备工作 - 大部分测试时间都花在生成数组上,而不是测试值。以下是正确的版本:https://jsbench.me/nyla6xchf4/1 - Miroslav Jonas
今天在较小的数组上,我仍然可以获得更快的性能,似乎在这方面没有什么改变。 - undefined

7

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 改变数组而引起的错误(代表我的朋友说的)。


这是正确的答案,谢谢你,Paul。 - David I. Samudio

2

@georg的回答对比较有帮助。我也很好奇.flat()在运行时的表现如何,结果是最差的。如果速度是优先考虑的因素,请勿使用.flat()。(这是我直到现在才意识到的事情)

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

  console.time("flat-big");
  for (i = 0; i < 1e2; i++) x = [[], big].flat();
  console.timeEnd("flat-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");

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


0
我参加了Georg的测试,并比较了在空数组和之前的数组中设置的性能结果。
以下是测试结果,我认为可能会对一些人感兴趣: 简而言之:使用特定的数组作为结果要快得多。

let big = (new Array(1e5)).fill(99);
let big2 = (new Array(1e5)).fill(99);
let res = []

console.time('concat-many');
for (i = 0; i < 1e2; i++) res = big.concat(big2)
console.timeEnd('concat-many');

console.time('concat-many-with-reset');
for (i = 0; i < 1e2; i++) big = big.concat(big2)
console.timeEnd('concat-many-with-reset');

res = []
big = (new Array(1e5)).fill(99);
console.time('spread-many');
for (i = 0; i < 1e2; i++) res = [...big, ...big2]
console.timeEnd('spread-many');


console.time('spread-many-with-reset');
for (i = 0; i < 1e2; i++) big = [...big, ...big2]
console.timeEnd('spread-many-with-reset');


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