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' };
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' };
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 };
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 }] }
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.
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
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 }] }
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)
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.
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? 🤔
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 putcopy
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.Let's hope they will implement a standardized copy method specifically made for javascript Class in the future 😁
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
.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?
🤔
Nice article!
Would love to see an example on the manual approach.
This question in stackoverflow has some answers with manual approach example.
stackoverflow.com/questions/445992...
i will update this article later