DEV Community

Cover image for The Right Way to Clone Nested Object/Array (Deep Clone) in Javascript
Syakir
Syakir

Posted on • Originally published at devaradise.com

The Right Way to Clone Nested Object/Array (Deep Clone) in Javascript

This post was originally published at 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.

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

console.log(original); // { name: 'Bob' };
Enter fullscreen mode Exit fullscreen mode

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

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

console.log(original); // { name: 'Alice' };
Enter fullscreen mode Exit fullscreen mode

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.

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

const editableForm = { ...initialForm };
Enter fullscreen mode Exit fullscreen mode

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.

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 }] }
Enter fullscreen mode Exit fullscreen mode

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

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

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.
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

3. Using loash cloneDeep (Library Approach)

If you're using the lodash library, you can leverage its 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

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 }] }
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (8)

Collapse
 
darkwiiplayer profile image
π’ŽWii πŸ³οΈβ€βš§οΈ

Deep-cloning structures can get difficult very quickly once you introduce OOP features; it's easy enough to mix anonymous objects being used as maps with more structured objects built from a class or constructor, and it's easy to imagine a case where you'd only want to clone the former but keep the latter as references.

Things get even more hairy when you want to actually clone instances of classes, but keep their oop semantics intact, as javascript doesn't have anything like a copy-constructor, which is technically what you'd need here.

Collapse
 
syakirurahman profile image
Syakir

For such case, we need to build a custom clone function. Or maybe a method inside the class that can copy all its properties/methods? πŸ€”

Collapse
 
darkwiiplayer profile image
π’ŽWii πŸ³οΈβ€βš§οΈ

Having a Constructor.copy(instance) method would be an easy fix, but for this to work you'd have to control both the deep-copy function and the classes to be copied. It would be cool if there was a standard and people would just put copy methods in their classes, but unfortunately that's not the case.

structuredClone would have been a good opportunity to define something like this, but it seems like they went the cheap route and just defined it as a serialisation+deserialisation internally.

Thread Thread
 
syakirurahman profile image
Syakir

Let's hope they will implement a standardized copy method specifically made for javascript Class in the future 😁

Collapse
 
yaezah profile image
yayza • Edited

This brings back dreadful memories of when I had this problem and never knew spread operator didn't clone nested objects. I hope whoever is having that same problem at the moment finds your post. I've since just created a utility function that uses the JSON method but it's good to know about structuredClone.

Collapse
 
syakirurahman profile image
Syakir

Yeah, that sucks.

Custom function that uses JSON method? You mean, for primivite type you clone with json, but for custom object, you clone it with other method?
πŸ€”

Collapse
 
prafulla-codes profile image
Prafulla Raichurkar

Nice article!
Would love to see an example on the manual approach.

Collapse
 
syakirurahman profile image
Syakir

This question in stackoverflow has some answers with manual approach example.
stackoverflow.com/questions/445992...

i will update this article later