Copy to New Array

Intro

A question popped into my head the other day - what is every way in JavaScript that I could make a copy of an array?

Say I had the following simple array:

const array = [1, 2, 3, 4];

What ways are available to me to create a new array with the same values? To get a result like:

const newArray = [1, 2, 3, 4];

I guess the most basic way of doing this would be to manually copy each value, one by one:

const array = [1, 2, 3, 4];

const newArray = [array[0], array[1], array[2], array[3]];
console.log(newArray);
// expected result: [1, 2, 3, 4]

This, of course, is a terrible approach. Imagine if array was 100 or 1,000 items long! We wouldn't want to have to type the index for that many items. There are a ton of ways to accomplish this, but before we dig into them, let's take a detour into shallow copying and deep copying.

Shallow copying vs. Deep copying

Take a look at the following code:

const array = [
  1,
  2,
  3,
  {
    value: 4,
  },
];

It's a pretty similar array, with the first three items being the numbers 1, 2, and 3. The fourth item, however, is an object with a single property, value, with the value of the number 4.

Now let's try and copy array using a while Loop:

const array = [
  1,
  2,
  3,
  {
    value: 4,
  },
];

const newArray = [];
let i = 0;

while (i < array.length) {
  newArray.push(array[i]);
  i++;
}

console.log(newArray);
// expected result: [1, 2, 3, { value: 4 }]

That's all well and good, but say we update the value of the object:

array[3].value = 100;

Would you expect the object within newArray to be { value: 100 }? Or would it stay as we originally assigned it, as { value: 4 }? Well let's take a look.

const array = [
  1,
  2,
  3,
  {
    value: 4,
  },
];

const newArray = [];
let i = 0;

while (i < array.length) {
  newArray.push(array[i]);
  i++;
}

array[3].value = 100;

console.log(array);
// expected result: [1, 2, 3, { value: 100 }]
console.log(newArray);
// expected result: [1, 2, 3, { value: 100 }]

The value of the object in newArray has changed from { value: 4 } to { value: 100 } in both array and newArray. We might say the value has mutated within newArray. This happens because copying an array using a while loop copies array items by reference, meaning the object in newArray is a reference that points to the object in array. When array's object's value gets updated, that updated object value shows up in both array and its copy, newArray. This is referred to as making a shallow copy of the array.

So how do we copy array, then update the object in array without updating the values within newArray? One way of doing this is using JSON.parse() and JSON.stringify():

const array = [
  1,
  2,
  3,
  {
    value: 4,
  },
];

const newArray = JSON.parse(JSON.stringify(array));

array[3].value = 100;

console.log(array);
// expected result: [1, 2, 3, { value: 100 }]
console.log(newArray);
// expected result: [1, 2, 3, { value: 4 }]

We'll get into what exactly JSON.parse() and JSON.stringify() are doing later on, but as you can see, this method copies each of the nested values within array into newArray, but when we update the value in the object in array, that update doesn't appear in newArray. This method of using JSON.parse() and JSON.stringify() copies the direct values themselves, not the references to the values, from array to newArray. This is a referred to as making a deep copy of the array.

Now that we understand the distinction between these two methods, let's look at each of the ways we can copy an array, both using shallow and deep copy methods.

Shallow copy methods

while Loop

We saw this above, but we can use a good old-fashioned while Loop to copy each value within one array by reference into a new array:

const array = [1, 2, 3, 4];
const newArray = [];

let i = 0;
while (i < array.length) {
  newArray.push(array[i]);
  i++;
}

console.log(newArray);
// expected result: [1, 2, 3, 4]

for Loop

Since we can use a while Loop with an i initialization value, we can also use a classic for Loop for pretty much the exact same effect:

const array = [1, 2, 3, 4];
const newArray = [];

for (let i = 0; i < array.length; i += 1) {
  newArray.push(array[i]);
}

console.log(newArray);
// expected result: [1, 2, 3, 4]

This is basically the same as using a while Loop; therefore, it probably shouldn't come as a surprise that this method also gives us a shallow copy.

Array.slice()

A super old school approach of making a shallow copy of an array is to use the Array.slice() method.

Array.slice() makes a shallow copy of a portion of an array. It accepts start and end numeric values as arguments, which correspond to the start and end portions of the array that you would like to copy. If you pass it a single argument, it copies from that index to the end of the array:

const array = [1, 2, 3, 4];

const newArray = array.slice(1);

console.log(newArray);
// expected result: [2, 3, 4]

If you pass it two arguments, it copies from the start index to the end index:

const array = [1, 2, 3, 4];

const newArray = array.slice(1, 3);

console.log(newArray);
// expected result: [2, 3]

However, if you pass it no arguments at all, just performs the shallow copy on the entire array:

const array = [1, 2, 3, 4];

const newArray = array.slice();

console.log(newArray);
// expected result: [1, 2, 3, 4]

... Spread Operator

We can use the ES6 ... spread operator to also make a shallow copy:

const array = [1, 2, 3, 4];

const newArray = [...array];

console.log(newArray);
// expected result: [1, 2, 3, 4]

Read more about Array Spread here.

Array.map()

We can also use the Array.map() method and return each element into the new array:

const array = [1, 2, 3, 4];

const newArray = array.map(element => element);

console.log(newArray);
// expected result: [1, 2, 3, 4]

This also gives us a shallow copy. Read more about the .map() Method here.

Array.filter()

We can also use the Array.filter() method, returning true for each element in the array, which makes a shallow copy of each item into the new array:

const array = [1, 2, 3, 4];

const newArray = array.filter(() => true);

console.log(newArray);
// expected result: [1, 2, 3, 4]

Read more about the .filter() Method here.

Array.reduce()

We can also use the Array.reduce() method to reduce each array into a new array:

const array = [1, 2, 3, 4];

const newArray = array.reduce((newArray, element) => {
  newArray.push(element);
  return newArray;
}, []);

console.log(newArray);
// expected result: [1, 2, 3, 4]

A bit of an absurd use of .reduce(), but it works here. This also gives us a shallow copy. Read more about the .reduce() Method here.

Array.concat() or Array.concat([])

Array.concat() allows you to merge two different arrays:

const array = [1, 2].concat([3, 4]);

console.log(array);
// expected result: [1, 2, 3, 4]

This method doesn't modify either of the original arrays, and instead produces a new array of the concatenated original arrays. Therefore, one of the nifty uses is using Array.concat() on an array and passing no argument to it. This concatenates nothing onto the original array, and assigns that to the new array, effectively copying the array:

const array = [1, 2, 3, 4];

const newArray = array.concat();

console.log(newArray);
// expected result: [1, 2, 3, 4]

Passing an empty array [] to .concat() works the exact same way:

const array = [1, 2, 3, 4];

const newArray = array.concat([]);

console.log(newArray);
// expected result: [1, 2, 3, 4]

These methods also give us a shallow copy.

Array.from()

We can also use the Array.from() method. Array.from() makes an array out of any iterable or array-like value:

const string = "howdy";

const array = Array.from(string);

console.log(array);
// expected result: ["h", "o", "w", "d", "y"]

If we use Array.from() on something that's already an array, then we get a shallow copy of that array:

const array = [1, 2, 3, 4];

const newArray = Array.from(array);

console.log(newArray);
// expected result: [1, 2, 3, 4]

Read more about the Array.from() Method here.

Deep copy methods

So now let's take a look at some deep copy methods. These tend to be a little bit more complicated than the shallow copy methods.

JSON.parse() and JSON.stringify()

We saw an example of this above, but we can use JSON.parse() and JSON.stringify() to copy an array:

const array = [1, 2, 3, 4];

const newArray = JSON.parse(JSON.stringify(array));

console.log(newArray);
// expected result: [1, 2, 3, 4]

So what's happening here? The first thing that happens is we pass array to JSON.stringify(), which converts that array to a JSON string:

const array = [1, 2, 3, 4];

const jsonValue = JSON.stringify(array);

console.log(jsonValue);
// expected result: "[1,2,3,4]"

We then pass that JSON string to JSON.parse(), which parses a JSON string and constructs a JavaScript value out of it:

const jsonValue = "[1,2,3,4]";

const newArray = JSON.parse(jsonValue);

console.log(newArray);
// expected result: [1, 2, 3, 4]

This method doesn't make a ton of sense for this simple array example, but it does enable us to perform a deep copy on our earlier, more complicated example:

const array = [
  1,
  2,
  3,
  {
    value: 4,
  },
];

const newArray = JSON.parse(JSON.stringify(array));

array[3].value = 100;

console.log(array);
// expected result: [1, 2, 3, { value: 100 }]
console.log(newArray);
// expected result: [1, 2, 3, { value: 4 }]

Let's break this down again, step by step. So we start by passing array to JSON.stringify(), creating a JSON string:

const array = [
  1,
  2,
  3,
  {
    value: 4,
  },
];

const jsonString = JSON.stringify(array);
console.log(jsonString);
// expected result: '[1,2,3,{"value":4}]'

Now we pass jsonString to JSON.parse() to parse that JSON string into a JavaScript value:

const jsonString = '[1,2,3,{"value":4}]';

const newArray = JSON.parse(jsonString);
console.log(newArray);
// expected result:[1, 2, 3, { value: 4 }]

Because we're copying the value of the object within array into just a basic JSON string, there's no actual reference copy. In this way, when we update value in the object within array it doesn't affect the values within newArray:

const array = [
  1,
  2,
  3,
  {
    value: 4,
  },
];

const newArray = JSON.parse(JSON.stringify(array));

array[3].value = 100;

console.log(array);
// expected result: [1, 2, 3, { value: 100 }]
console.log(newArray);
// expected result: [1, 2, 3, { value: 4 }]

Recursion

We can also use recursion to copy an array:

const array = [1, 2, 3, 4];

function clone(value){
  if (Array.isArray(value)) {
    return value.map(item => clone(item));
  } else if (typeof value === 'object' && value !== null) {
    const cloneObject = {};
    for (let key in value) {
      if (value.hasOwnProperty(key)) {
        cloneObject[key] = clone(value[key]);
      }
    }
    return cloneObject;
  } else {
    return value;
  }
}

const newArray = clone(array);

console.log(newArray);
// expected result: [1, 2, 3, 4]

We're using a helper function, clone, which accepts a value argument.

The first thing that this function does is check whether value is an array using the Array.isArray() method. If so, it maps over each item, returning a recursive call of clone() on that item.

If value is an object and that object's value isn't null (weird quirk of null's typeof being object), then we instantiate an object, cloneObject, and add each item within the original object into cloneObject. We are yet again recursively calling clone() in this block to ensure we are copying by value and not by reference.

Lastly, if value is neither an array nor a non-null object, we return the value itself. It's complicated, but it allows us to perform deep clones:

const array = [
  1,
  2,
  3,
  {
    value: 4,
  },
];

function clone(value){
  if (Array.isArray(value)) {
    return value.map(item => clone(item));
  } else if (typeof value === 'object' && value !== null) {
    const cloneObject = {};
    for (let key in value) {
      if (value.hasOwnProperty(key)) {
        cloneObject[key] = clone(value[key]);
      }
    }
    return cloneObject;
  } else {
    return value;
  }
}

const newArray = clone(array);

array[3].value = 100;

console.log(array);
// expected result: [1, 2, 3, { value: 100 }]
console.log(newArray);
// expected result: [1, 2, 3, { value: 4 }]

structuredClone()

The last method I'll cover here is the structuredClone() global function. This is a relatively new function that was added to the language around 2022. It accepts an array and returns a deep copy of that array:

Like all of our deep copy methods, it works both for simple arrays...

const array = [1, 2, 3, 4];

const newArray = structuredClone(array);

console.log(newArray);
// expected result: [1, 2, 3, 4]

...and for our more complex arrays:

const array = [
  1,
  2,
  3,
  {
    value: 4,
  },
];

const newArray = structuredClone(array);

array[3].value = 100;

console.log(array);
// expected result: [1, 2, 3, { value: 100 }]
console.log(newArray);
// expected result: [1, 2, 3, { value: 4 }]

This is by and far (to me at least) the cleanest, most understandable of the deep clone approaches.

Summary

  • Shallow copying copies an array by reference, so internal values can be updated, mutating the copied array when the reference item updates. These methods are typically simpler than deep copy methods, and they include:
    • while Loop
    • for Loop
    • Array.slice()
    • ... Spread Operator
    • Array.map()
    • Array.filter()
    • Array.reduce()
    • Array.concat() or Array.concat([])
    • Array.from()
  • Deep copying copies an array by value, so updating items in the original array (or their references) do not mutate the new array's item values. These methods are typically a little more complicated, and they include:
    • JSON.parse() and JSON.stringify()
    • Recursion
    • structuredClone()

So there you have it. Did I miss anything? I'm sure that I did. I don't really use lodash, but I know they have a _.cloneDeep() method that is probably pretty reliable. Someone on Stack Overflow also provided this intense-looking clone() function that accounts for just about every type of JavaScript object:

function clone(src, deep) {
  var toString = Object.prototype.toString;
  if (!src && typeof src != "object") {
    // Any non-object (Boolean, String, Number), null, undefined, NaN
    return src;
  }

  // Honor native/custom clone methods
  if (src.clone && toString.call(src.clone) == "[object Function]") {
    return src.clone(deep);
  }

  // DOM elements
  if (src.nodeType && toString.call(src.cloneNode) == "[object Function]") {
    return src.cloneNode(deep);
  }

  // Date
  if (toString.call(src) == "[object Date]") {
    return new Date(src.getTime());
  }

  // RegExp
  if (toString.call(src) == "[object RegExp]") {
    return new RegExp(src);
  }

  // Function
  if (toString.call(src) == "[object Function]") {
    //Wrap in another method to make sure == is not true;
    //Note: Huge performance issue due to closures, comment this :)
    return (function(){
        src.apply(this, arguments);
    });
  }

  var ret, index;
  //Array
  if (toString.call(src) == "[object Array]") {
    //[].slice(0) would soft clone
    ret = src.slice();
    if (deep) {
      index = ret.length;
      while (index--) {
        ret[index] = clone(ret[index], true);
      }
    }
  }
  //Object
  else {
    ret = src.constructor ? new src.constructor() : {};
    for (var prop in src) {
      ret[prop] = deep
        ? clone(src[prop], true)
        : src[prop];
    }
  }
  return ret;
};

If you know of a method for copying an array that I missed in this post, please feel free to tell me about it at joey@joeyreyes.dev. I'll add it here and give you credit.