The problem with array cloning in Javascript (and how to solve it)
Tags: todayisearched, javascript, array, deepclone
Published: 2020-06-23 06:34:12

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

```javascript
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:

```javascript
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:

```javascript
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice):
> 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:

```javascript
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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign):
> 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?...

```javascript
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](https://dev-to-uploads.s3.amazonaws.com/i/5l9cnxh20p0wia49at3n.png)

```javascript
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**:

```javascript
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...

{% codesandbox 2n5e8 %}

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

```javascript
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 🙄...

{% codesandbox hl527 %}

![](https://media.giphy.com/media/aZ3LDBs1ExsE8/source.gif)

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](https://dev-to-uploads.s3.amazonaws.com/i/678pm5kch5x8tz0d9goc.png)

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

```javascript
function copy(array) {
  return JSON.parse(JSON.stringify(array));
}
```
And now let's write some tests:

```javascript
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 ...

{% codesandbox e5b3x %}

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.