The Right Way to Clone Nested Object/Array (Deep Clone) in Javascript
Tags: javascript, webdev, beginners, frontend
Published: 2024-06-17 11:18:00

This post was originally published at [https://devaradise.com/deep-clone-nested-object-array-in-javascript](https://devaradise.com/deep-clone-nested-object-array-in-javascript)

As you might know, Javascript uses pass-by-reference when passing an object, array, or function to a new variable. When you pass an object, array, or function to a new variable, a reference (memory address) to the object is passed.

Any modification to the object's properties in the new variable will be reflected in the original object since they both point to the same memory location.

```javascript
const original = { name: 'Alice' };
const newVariable = original;
newVariable.name = 'Bob';

console.log(original); // { name: 'Bob' };
```

To solve this issue we can use `Object.assign()` or `{...}` spread operator to clone the original object.

```javascript
const original = { name: 'Alice' };
const newVariable = Object.assign({}, original);
newVariable.name = 'Bob';

console.log(original); // { name: 'Alice' };
```

However, this solution only works for a simple object or flat array. Meanwhile, in real-world use cases, we often have to deal with complex, nested objects and arrays.

## Object.assign and Spread Operator Are not Enough!

Recently, I encountered a bug in my project codebase that happened because the previous developer used the spread operator (`{...}`) to clone a nested object. The object structure is like this.

```javascript
const initialForm = { name: 'Alice', items: [{ value: 1 }] };

const editableForm = { ...initialForm };
```

The expected outcome from the codes is we want to have 2 objects, where the `initialForm` is the previous data before editing, and `editableForm` is the object that will be bound to a form component where the user can modify it.

If a user clicks a submit button in the form, we want to compare both objects to see if the user makes any changes. It works as expected when the user changes the name, because well they are 2 different objects.

But when we added an item or changed the item value without changing the name, the comparison didn't detect any change.

```javascript
const editableForm = { ...initialForm };
editableForm.item.push({ value: 2 });

console.log(JSON.stringify(editableForm) === JSON.stringify(initialForm)); // true
console.log(initialForm); // { name: 'Alice', items: [{ value: 1 }, { value: 2 }] }
```

It turns out that the `{ ...initialForm }` didn't clone the `items` array value to `editableForm`. The objects `initialForm` and `editableForm` are indeed 2 different objects, but they refer to the same `items` array value in memory.

To fix this issue, we browsed the internet and found some methods to clone nested objects/arrays properly.

## Modern Ways to Clone Nested Objects/Arrays

Here are some effective methods for deep cloning in modern JavaScript:

### 1. Using the new `structuredClone` function.

`structuredClone` is the newest and most recommended approach. It's built into modern browsers and offers several advantages:

- **Deep Cloning**: Handles nested structures effectively.
- **Circular References**: Can handle circular references within the object.
- **Data Type Support**: Supports data types like Dates, Sets, Maps, and more.

More details about `structuredClone` can be found in [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone#description)

#### Props

- Modern, robust, efficient, handles complex data types and circular references.

#### Cons

- Limited browser support (might require a polyfill for older browsers).
- No support for an object with function and will return a `DataCloneError`.

#### Examples

```javascript
const original = { name: 'Alice', data: [{ value: 3 }] };

const cloned = structuredClone(original);
cloned.data[0].value = 2;

console.log(original); // { name: 'Alice', data: [{ value: 3 }] }
console.log(cloned); // { name: 'Alice', data: [{ value: 2 }] }

const objWithFunction = { name: 'Alice', action: () => {} };
const objWithFunctionClone = structuredClone(objWithFunction);
// DataCloneError: Failed to execute 'structuredClone' on 'Window': () => {} could not be cloned.
```

### 2. Using JSON Serialization (`JSON.stringify` & `JSON.parse`)

This method leverages JSON conversion. It converts the object to a JSON string and then parses it back into a JavaScript object. While effective, it has limitations:

#### Pros:

- Simple and widely supported approach.

#### Cons:

- Loss of Information: Certain data types like Dates, Functions, Set, Map, and custom objects might lose their original properties during conversion.
- Circular References: Cannot handle circular references by default.

#### Example

```javascript
const original = {
	name: 'Alice',
	date: new Date()
};
const cloned = JSON.parse(JSON.stringify(original));

console.log(original); // {"name":"Alice", "date": Sat Jun 15 2024 12:38:56 GMT+0700 (Western Indonesia Time) }
console.log(cloned); // {"name":"Alice", "date": "2024-06-15T05:37:06.172Z" }

original.circular = original;
const circularObj = JSON.parse(JSON.stringify(original));
// TypeError: Converting circular structure to JSON
```

### 3. Using loash `cloneDeep` (Library Approach)

If you're using the lodash library, you can leverage its [`cloneDeep`](https://lodash.com/docs/4.17.15#cloneDeep) function for deep cloning. It offers similar functionality to `structuredClone` but requires an additional library.

#### Pros

- Convenient if you're already using lodash, offers deep cloning functionality.
- Widely supported

#### Cons:

- Introduces an external dependency.

#### Examples

```javascript
import { cloneDeep } from 'lodash';

const original = { name: 'Alice', data: [{ value: 3 }] };

const cloned = cloneDeep(original);
cloned.data[0].value = 2;

console.log(original); // { name: 'Alice', data: [{ value: 3 }] }
console.log(cloned); // { name: 'Alice', data: [{ value: 2 }] }
```

### 4. Manual Deep Clone (Recursive Approach)

For more control and handling of specific data types, you can write a recursive function to traverse the object structure and create new copies at each level. This approach offers flexibility but requires more coding effort.

## Choosing the Right Method

The best method for deep cloning depends on your specific needs and browser compatibility. Here's a quick guide:

- Use `structuredClone` for the most modern and robust solution, or when working in Nodejs environment
- Use JSON serialization for a simpler approach, but be aware of limitations.
- Use `lodash.cloneDeep` if you're already using lodash.
- Use a manual recursive approach for fine-grained control or handling specific data types.

## Conclusion

By understanding these different methods for deep cloning nested objects and arrays in JavaScript, you can ensure your code works as intended and avoid unintended modifications to the original data. Choose the method that best suits your project requirements and browser compatibility.

Do you have another method to clone nested objects and arrays? Share your opinion in the comment below.

Have a nice day!