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!