loading...
Cover image for The problem with array cloning in Javascript (and how to solve it)

The problem with array cloning in Javascript (and how to solve it)

jorge_rockr profile image Jorge Ramón ・4 min read

Suppose you have to write a function in Javascript that given an array as parameter, it returns a copy. For example:

function copy(array) {
  // magic code...
}

const array = [1,2,3];
const newArray = copy(array);
console.log(newArray); // [1,2,3]

This function can be very useful because you can modify the new array without modifying the old one:

newArray.push(4);
newArray[0] = 0;

console.log(array); // [1,2,3]
console.log(newArray); // [0,2,3,4]

Very easy, right? I'm pretty sure you already got one or two solutions in mind while reading this, I have 3 solutions to share with you, let's check them out.

1. Using a for/while loop

The simplest way is create a new empty array and use a loop to push each element from the old array to the new one:

function copyWithLoop(array) {
  const newArray = [];

  for (let item of array) {
    newArray.push(item);
  }

  return newArray;
}

const array = [1,2,3];
const newArray = copyWithLoop(array);
console.log(newArray); // [1,2,3];

2. Using Array.prototype.slice method

According to MDN web docs:

The slice() method returns a shallow copy of a portion of an array into a new array object selected from start to end (end not included) where start and end represent the index of items in that array. The original array will not be modified.

OMG 😱 It's exactly what we are looking for. Let's give it a try:

function copyWithSlice(array) {
  return array.slice();
}

const array = [1,2,3];
const newArray = copyWithSlice(array);
console.log(newArray); // [1,2,3];

3. Using Object.assign method

According to MDN web docs:

The Object.assign() method copies all enumerable own properties from one or more source objects to a target object. It returns the target object.

So, if it works with Object, it should work with Array too, right?...

function copyWithAssign(array) {
  return Object.assign([], array);
}

const array = [1,2,3];
const newArray = copyWithAssign(array);
console.log(newArray); // [1,2,3];

And yep, it works too 😱😱! How can we do it better?

3. Use ES2015 Spread Operator

Spread Operator was introduced in ES2015 and it allows any iterable element (such as an array or object) to be "expanded" in places where zero or more arguments are expected.

Spread Operator Example

function copyWithSpread(array) {
  return [...array];
}

const array = [1,2,3];
const newArray = copyWithSpread(array);
console.log(newArray); // [1,2,3];

And guess what? It works 🎉✨!

All solutions look good but just to be sure, let's write some tests using Jest:

import {
  copyWithLoop,
  copyWithSlice,
  copyWithAssign,
  copyWithSpread
} from "./lib";

describe("copyWithLoop", function() {
  test("Testing an empty array", function() {
    const array = [];
    const newArray = copyWithLoop(array);

    newArray.push(0);

    expect(newArray).not.toEqual(array);
  });

  test("Testing a populated array", function() {
    const array = [1, 2, 3];
    const newArray = copyWithLoop(array);

    newArray.push(0);
    newArray[0] = -1;

    expect(newArray).not.toEqual(array);
  });
});

describe("copyWithSlice", function() {
  test("Testing an empty array", function() {
    const array = [];
    const newArray = copyWithSlice(array);

    newArray.push(0);

    expect(newArray).not.toEqual(array);
  });

  test("Testing a populated array", function() {
    const array = [1, 2, 3];
    const newArray = copyWithSlice(array);

    newArray.push(0);
    newArray[0] = -1;

    expect(newArray).not.toEqual(array);
  });
});

describe("copyWithAssign", function() {
  test("Testing an empty array", function() {
    const array = [];
    const newArray = copyWithAssign(array);

    newArray.push(0);

    expect(newArray).not.toEqual(array);
  });

  test("Testing a populated array", function() {
    const array = [1, 2, 3];
    const newArray = copyWithAssign(array);

    newArray.push(0);
    newArray[0] = -1;

    expect(newArray).not.toEqual(array);
  });
});

describe("copyWithSpread", function() {
  test("Testing an empty array", function() {
    const array = [];
    const newArray = copyWithSpread(array);

    newArray.push(0);

    expect(newArray).not.toEqual(array);
  });

  test("Testing a populated array", function() {
    const array = [1, 2, 3];
    const newArray = copyWithSpread(array);

    newArray.push(0);
    newArray[0] = -1;

    expect(newArray).not.toEqual(array);
  });
});

And the result is...

All tests passed 😁... but wait! I didn't test Objects but meh, it should be the same 🙄.

import {
  copyWithLoop,
  copyWithSlice,
  copyWithAssign,
  copyWithSpread
} from "./lib";

describe("copyWithLoop", function() {
  // Testing an empty array still passes :)

  test("Testing a populated array", function() {
    const array = [{ a: 0 }, { b: 1 }, { c: 2 }];
    const newArray = copyWithLoop(array);

    newArray[0].a = -1;

    expect(newArray).not.toEqual(array);
  });
});

describe("copyWithSlice", function() {
  // Testing an empty array still passes :)

  test("Testing a populated array", function() {
    const array = [{ a: 0 }, { b: 1 }, { c: 2 }];
    const newArray = copyWithSlice(array);

    newArray[0].a = -1;

    expect(newArray).not.toEqual(array);
  });
});

describe("copyWithAssign", function() {
  // Testing an empty array still passes :)

  test("Testing a populated array", function() {
    const array = [{ a: 0 }, { b: 1 }, { c: 2 }];
    const newArray = copyWithAssign(array);

    newArray[0].a = -1;

    expect(newArray).not.toEqual(array);
  });
});

describe("copyWithSpread", function() {
  // Testing an empty array still passes :)

  test("Testing a populated array", function() {
    const array = [{ a: 0 }, { b: 1 }, { c: 2 }];
    const newArray = copyWithSpread(array);

    newArray[0].a = -1;

    expect(newArray).not.toEqual(array);
  });
});

And the obvious result is 🙄...

What?! How?! 🤯

Well, the solutions really created a whole new Array (that's why empty array tests pass) but both arrays share the same object references 🤯:

Arrays sharing the same reference

After some research I found out the solution and is... convert the array to a string and convert it back to an array.

Yep, you read it right, this is by far the best solution at the moment. Let's see if it really works!

4. Using JSON.parse + JSON.stringify methods

function copy(array) {
  return JSON.parse(JSON.stringify(array));
}

And now let's write some tests:

import { copy } from "./lib";

describe("copy", function() {
  test("Testing an empty array", function() {
    const array = [];
    const newArray = copy(array);

    newArray.push({});

    expect(newArray).not.toEqual(array);
  });

  test("Testing a populated array", function() {
    const array = [{ a: 0 }, { b: 1 }, { c: 2 }];
    const newArray = copy(array);

    newArray[0].a = -1;
    newArray.push({ d: 3 });

    expect(newArray).not.toEqual(array);
  });
});

And the result is ...

All tests passed 🎉🎉✨✨!

This function can be used to copy objects too, BTW.

And there you have it 😎, it's funny how a very simple problem can be very tricky to solve.

That's all for now, folks! Hope you learn something new today and see you in the next post.

Posted on by:

jorge_rockr profile

Jorge Ramón

@jorge_rockr

Full Stack Developer at @Globant. Javascript is all that you need.

Discussion

markdown guide