133

What is the difference between spread operator and 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));

Both results are same. So, what kind of scenarios we want to use them? And which one is best for performance?

Penny Liu
  • 15,447
  • 5
  • 79
  • 98
Ramesh Rajendran
  • 37,412
  • 45
  • 153
  • 234

7 Answers7

221

concat and spreads are very different when the argument is not an array.

When the argument is not an array, concat adds it as a whole, while ... tries to iterate it and fails if it can't. Consider:

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' ]

Here, concat treats the string atomically, while ... uses its default iterator, char-by-char.

Another example:

x = 99;

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

Again, for concat the number is an atom, ... tries to iterate it and fails.

Finally:

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 makes no attempt to iterate the generator and appends it as a whole, while ... nicely fetches all values from it.

To sum it up, when your arguments are possibly non-arrays, the choice between concat and ... depends on whether you want them to be iterated.

The above describes the default behaviour of concat, however, ES6 provides a way to override it with Symbol.isConcatSpreadable. By default, this symbol is true for arrays, and false for everything else. Setting it to true tells concat to iterate the argument, just like ... does:

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' ]

Performance-wise concat is faster, probably because it can benefit from array-specific optimizations, while ... has to conform to the common iteration protocol. Timings:

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');
georg
  • 211,518
  • 52
  • 313
  • 390
  • 21
    This should be the correct answer. Less subjective than bergi's answer. – lintuxvi May 14 '20 at 16:10
  • 1
    for some reason, `array.concat` was not working for me in a typescript project. So I had to use `array.push(...spread)`. I found [this link](https://github.com/microsoft/TypeScript/issues/10479#issuecomment-241533955) which talks about the concat issue ion typescript which suggest to use `([] as any[]).concat(myArray)`, but that doesn't seem to work either. – Gangula Jun 12 '23 at 12:13
79

Well console.log(['one', 'two', 'three', 'four', 'five']) has the same result as well, so why use either here? :P

In general you would use concat when you have two (or more) arrays from arbitrary sources, and you would use the spread syntax in the array literal if the additional elements that are always part of the array are known before. So if you would have an array literal with concat in your code, just go for spread syntax, and just use concat otherwise:

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

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

Also the two alternatives behave quite differently when dealing with non-array values.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Is it possible to do concat or spread an array in between an another array? – Ramesh Rajendran Feb 19 '18 at 12:08
  • @RameshRajendran In between two other arrays, sure. "In between *an* other array", not sure what you mean. – Bergi Feb 19 '18 at 12:10
  • For example I can use spread operators in between the array objects. `['one',...parts, 'two', 'three'];`. now `four and five` is move to second potion. Is it possible in `concat()` – Ramesh Rajendran Feb 19 '18 at 12:12
  • 1
    @RameshRajendran The equivalent to that would be `['one'].concat(parts, ['two', 'three'])` (or `['one'].concat(parts).concat(['two', 'three'])` if you don't want to pass multiple arguments) – Bergi Feb 19 '18 at 12:13
  • 10
    FWIW, there's a measurable performance difference. See https://jsperf.com/spread-vs-concat-vs-push – broofa Jan 31 '19 at 23:08
  • Your second example could have also been written `a.concat(x).concat(y)`, which is imo just as silly, so I'd personally include requiring multiple concats as additional qualifier for your spread criteria. – Drazen Bjelovuk Sep 06 '19 at 20:35
  • 2
    @DrazenBjelovuk `.concat(x)` makes the reader assume that `x` is an array as well. Sure, `concat` can handle non-array values as well, but imo that's not its main mode of operation. Especially if `x` is an arbitrary (unknown) value, you would need to write `.concat([x])` to make sure it always works as intended. And as soon as you have to write an array literal anyway, I say that you should just use spread syntax instead of `concat`. – Bergi Sep 06 '19 at 20:52
  • @DrazenBjelovuk Of course there is: when `x` is an array (or some other [concatSpreadable object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/isConcatSpreadable)), the one expression would spread it and the other would append `x` as a single element (the desired result). – Bergi Sep 06 '19 at 21:02
  • @Bergi Ah, I see what you're getting at. `a.concat(x).concat(y)` would not necessarily be equivalent given that either `x` or `y` were arrays themselves. Very good point. Though by that logic, if `a` were unknown and your intention was to simply tack it on in one piece in the case that it wasn't an array, your spread alternative would break down. – Drazen Bjelovuk Sep 06 '19 at 21:47
  • @DrazenBjelovuk In the example, `a` and `b` are supposed to be arrays of (arbitrary) elements, and `x` and `y` are supposed to be (arbitrary) element values. – Bergi Sep 06 '19 at 21:52
  • @Bergi I'm just saying in your second example, if `a` could be either an array or non-array value (unknown) and your intention were to tack it on as a single element, your criteria of "just use spread if your statement includes an array literal" wouldn't fit the bill. It's non-categorical. – Drazen Bjelovuk Sep 07 '19 at 00:47
  • @DrazenBjelovuk Same in the first example: if `a` wasn't an array, then `a.concat(…)` didn't make any sense. Yes, to tack on a single element, I would never pass it to `concat` directly but always wrap it in an array literal - and that's where spread syntax comes in. – Bergi Sep 07 '19 at 00:55
  • @Bergi Sorry, I think I was unclear in describing the scenario. To clarify: `a` is either an array or non-array (unknown), if it's an array, I want to spread it, if it's not, I want to tack it on. `[x, y].concat(a)` satisfies this, `[x, y, ...a]` does not. Following your rule of thumb here would lead to undesired behaviour. – Drazen Bjelovuk Sep 07 '19 at 01:28
  • @DrazenBjelovuk Well that's why it's only a rule of thumb, not a law set in stone: if you have weird requirements, you're an exception to the rule :-) Also see the last sentence of my answer as a clear disclaimer. – Bergi Sep 07 '19 at 01:53
69

.concat() is faster, otherwise they work the same assuming both arguments are lists. Here's the time it takes to merge two arrays with 10 million elements each (lower is better):

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

I ran this code on an M1 MacBook Air with 8GB of RAM:

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`);
Boris Verkhovskiy
  • 14,854
  • 11
  • 100
  • 103
  • 16
    Thanks, this is actually a useful answer in terms of what actually matters. – King Friday Jun 06 '20 at 16:22
  • 12
    I rarely have 10.000.000 elements. Would rather/also like to see a comparison of merging 10, 100 or 1000 elements, and doing the merge many times. – daniero Aug 19 '20 at 10:34
14

The one difference I think is valid is that using spread operator for large array size will give you error of Maximum call stack size exceeded which you can avoid using the concat operator.

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)
Ankit Agarwal
  • 30,378
  • 5
  • 37
  • 62
  • 2
    This use of spread syntax (function call) is not what was asked about (array literal). – Bergi Feb 19 '18 at 12:06
  • I know that, the OP has not `push` the element. But we usually `push` the element in array so I am trying to show the consequences when we will use the push with spread. – Ankit Agarwal Feb 19 '18 at 12:10
  • 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. – Luis Villavicencio Oct 03 '18 at 16:05
  • 2
    This is not relevant to the question and its kind of misleading – bumbeishvili Jan 01 '21 at 14:44
7

Update:

Concat is now always faster than spread. The following benchmark shows both small and large-size arrays being joined: 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)

Previous:

Although some of the replies are correct when it comes to performance on big arrays, the performance is quite different when you are dealing with small arrays.

You can check the results for yourself at https://jsperf.com/spread-vs-concat-size-agnostic.

As you can see, spread is 50% faster for smaller arrays, while concat is multiple times faster on large arrays.

Miroslav Jonas
  • 5,407
  • 1
  • 27
  • 41
  • 5
    Link is broken—and for this reason, it's always best to provide a summary of the linked content in case the link ever breaks. – Scott Schupbach May 27 '21 at 17:44
  • not sure if this is correct. see https://stackoverflow.com/a/74161832/3370568 – Wajahath Oct 22 '22 at 12:21
  • It's not fully correct anymore due to changes in the browser, but the linked answer is also not correct. Both tests include preparation - most of the test's duration is spent on array generation instead of testing the values. Here is the correct version https://jsbench.me/nyla6xchf4/1 – Miroslav Jonas Nov 07 '22 at 15:19
7

There is one very important difference between concat and push in that the former does not mutate the underlying array, requiring you to assign the result to the same or different array:

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' ]

I've seen bugs caused by the assumption that concat changes the array (talking for a friend ;).

PaulJNewell
  • 239
  • 3
  • 9
2

The answer by @georg was helpful to see the comparison. I was also curious about how .flat() would compare in the running and it was by far the worst. Don't use .flat() if speed is a priority. (Something I wasn't aware of until now)

  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");
Kuklaph
  • 57
  • 1
  • 10