How to extend an existing JavaScript array with another array, without creating a new array

Viewed 866964

There doesn't seem to be a way to extend an existing JavaScript array with another array, i.e. to emulate Python's extend method.

I want to achieve the following:

>>> a = [1, 2]
[1, 2]
>>> b = [3, 4, 5]
[3, 4, 5]
>>> SOMETHING HERE
>>> a
[1, 2, 3, 4, 5]

I know there's a a.concat(b) method, but it creates a new array instead of simply extending the first one. I'd like an algorithm that works efficiently when a is significantly larger than b (i.e. one that does not copy a).

Note: This is not a duplicate of How to append something to an array? -- the goal here is to add the whole contents of one array to the other, and to do it "in place", i.e. without copying all elements of the extended array.

20 Answers

Overview

  • a.push(...b) - limited, fast, modern syntax
  • a.push.apply(a, b) - limited, fast
  • a = a.concat(b) unlimited, slow if a is large
  • for (let i in b) { a.push(b[i]); } - unlimited, slow if b is large

Each snippet modifies a to be extended with b.

The "limited" snippets pass each array element as an argument, and the maximum number of arguments you can pass to a function is limited. From that link, it seems that a.push(...b) is reliable until there are about 32k elements in b (the size of a does not matter).

Relevant MDN documentation: spread syntax, .apply(), .concat(), .push()

Speed considerations

Every method is fast if both a and b are small, so in most web applications you'll want to use push(...b) and be done with it.

If you're handling more than a few thousand elements, what you want to do depends on the situation:

  • you're adding a few elements to a large array
    push(...b) is very fast
  • you're adding many elements to a large array
    concat is slightly faster than a loop
  • you're adding many elements to a small array
    concat is much faster than a loop
  • you're usually adding only a few elements to any size array
    → loops are about as fast as the limited methods for small additions, but will never throw an exception even if it is not the most performant when you add many elements
  • you're writing a wrapper function to always get the maximum performance
    → you'll need to check the lengths of the inputs dynamically and choose the right method, perhaps calling push(...b_part) (with slices of the big b) in a loop.

This surprised me: I thought a=a.concat(b) would be able to do a nice memcpy of b onto a without bothering to do individual extend operations as a.push(...b) would have to do, thus always being the fastest. Instead, a.push(...b) is much, much faster especially when a is large.

The speed of different methods was measured in Firefox 88 on Linux using:

a = [];
for (let i = 0; i < Asize; i++){
  a.push(i);
}
b = [];
for (let i = 0; i < Bsize; i++){
  b.push({something: i});
}
t=performance.now();
// Code to test
console.log(performance.now() - t);

Parameters and results:

 ms | Asize | Bsize | code
----+-------+-------+------------------------------
 ~0 |  any  |  any  | a.push(...b)
 ~0 |  any  |  any  | a.push.apply(a, b)
480 |   10M |    50 | a = a.concat(b)
  0 |   10M |    50 | for (let i in b) a.push(b[i])
506 |   10M |  500k | a = a.concat(b)
882 |   10M |  500k | for (let i in b) a.push(b[i])
 11 |    10 |  500k | a = a.concat(b)
851 |    10 |  500k | for (let i in b) a.push(b[i])

Note that a Bsize of 500 000 is the largest value accepted by all methods on my system, that's why it is smaller than Asize.

All tests were run multiple times to see if the results are outliers or representative. The fast methods are almost immeasurable in just one run using performance.now(), of course, but since the slow methods are so obvious and the two fast methods both work on the same principle, we needn't bother repeating it a bunch of times to split hairs.

The concat method is always slow if either array is large, but the loop is only slow if it has to do a lot of function calls and doesn't care how large a is. A loop is thus similar to push(...b) or push.apply for small bs but without breaking if it does get large; however, when you approach the limit, concat is a bit faster again.

This solution works for me (using the spread operator of ECMAScript 6):

let array = ['my', 'solution', 'works'];
let newArray = [];
let newArray2 = [];
newArray.push(...array); // Adding to same array
newArray2.push([...array]); // Adding as child/leaf/sub-array
console.log(newArray);
console.log(newArray2);

as the top voted answer says, a.push(...b) is probably the correct answer taking into account the size limit issue.

On the other hand, some of the answers on performance seem out of date.

These numbers below are for 2022-05-20

enter image description here

enter image description here

enter image description here

from here

At appears that push is fastest across the board in 2022. That may change in the future.

Answers ignoring the question (generating a new array) are missing the point. Lots of code might need/want to modify an array in place given there can be other references to the same array

let a = [1, 2, 3];
let b = [4, 5, 6];
let c = a;
a = a.concat(b);    // a and c are no longer referencing the same array

Those other references could be deep in some object, something that was captured in a closure, etc...

As a probably bad design but as an illustration, imagine you had

const carts = [
  { userId: 123, cart: [item1, item2], },
  { userId: 456, cart: [item1, item2, item3], },
];

and a function

function getCartForUser(userId) {
  return customers.find(c => c.userId === userId);
}

Then you want to add items to the cart

const cart = getCartForUser(userId);
if (cart) {
  cart.concat(newItems);      // FAIL 
  cart.push(...newItems);     // Success! 
}

As an aside, the answers suggesting modifying Array.prototype are arguably bad adivce. Changing the native prototypes is bascially a landmine in your code. Another implementation maybe be different than yours and so it will break your code or you'll break their code expecting the other behavior. This includes if/when a native implmentation gets added that clashes with yours. You might say "I know what I'm using so no issue" and that might be true at the moment and you're a single dev but add a second dev and they can't read your mind. And, you are that second dev in a few years when you've forgotten and then graft some other library (analytics?, logging?, ...) on to your page and forget the landmind you left in the code.

This is not just theory. There are countless stories on the net of people running into these landmines.

Arguably there are just a few safe uses for modifying a native object's prototype. One is to polyfill an existing and specified implementation in an old browser. In that case, the spec is defined, the spec is implemented is shipping in new browsers, you just want to get the same behavior in old browsers. That's pretty safe. Pre-patching (spec in progress but not shipping) is arguably not safe. Specs change before shipping.

I'm adding this answer, because despite the question stating clearly without creating a new array, pretty much every answer just ignores it.

Modern JavaScript works well with arrays and alike as iterable objects. This makes it possible to implement a version of concat that builds upon that, and spans the array data across its parameters logically.

The example below makes use of iter-ops library that features such logic:

import {pipe, concat} from 'iter-ops';

const i = pipe(
    originalArray,
    concat(array2, array3, array4, ...)
); //=> Iterable

for(const a of i) {
    console.log(a); // joint values from all arrays
}

Above, no new array is created. Operator concat will iterate through the original array, then will automatically continue into array2, then array3, and so on, in the specified order.

This is the most efficient way of joining arrays in terms of performance and memory usage.

And if, at the end, you decide to convert it into an actual physical array, you can do so via the standard spread operator:

const fullArray = [...i]; // pulls all values from iterable, into a new array

Another option, if you have lodash installed:

 import { merge } from 'lodash';

 var arr1 = merge(arr1, arr2);

Use Array.extend instead of Array.push for > 150,000 records.

if (!Array.prototype.extend) {
  Array.prototype.extend = function(arr) {
    if (!Array.isArray(arr)) {
      return this;
    }

    for (let record of arr) {
      this.push(record);
    }

    return this;
  };
}

You can do that by simply adding new elements to the array with the help of the push() method.

let colors = ["Red", "Blue", "Orange"];
console.log('Array before push: ' + colors);
// append new value to the array
colors.push("Green");
console.log('Array after push : ' + colors);

Another method is used for appending an element to the beginning of an array is the unshift() function, which adds and returns the new length. It accepts multiple arguments, attaches the indexes of existing elements, and finally returns the new length of an array:

let colors = ["Red", "Blue", "Orange"];
console.log('Array before unshift: ' + colors);
// append new value to the array
colors.unshift("Black", "Green");
console.log('Array after unshift : ' + colors);

There are other methods too. You can check them out here.

Super simple, does not count on spread operators or apply, if that's an issue.

b.map(x => a.push(x));

After running some performance tests on this, it's terribly slow, but answers the question in regards to not creating a new array. Concat is significantly faster, even jQuery's $.merge() whoops it.

https://jsperf.com/merge-arrays19b/1

Related