JavaScript Memory and Execution Mastery
Deep Cloning and Immutability
Deep Cloning and Immutability
In the previous section, we saw how shallow copies, like those made with the spread syntax (...) or Object.assign(), only duplicate the top level of an object. Nested objects and arrays are still shared by reference, which can lead to unintentional side effects. When you need a completely independent copy of an object, you need to perform a deep clone.
A deep clone creates a new object and recursively copies every nested object and property. The new object shares no references with the original. Let's explore how to achieve this.
The Old Way: JSON Trickery
For a long time, a common and quick way to create a deep clone was to use a combination of JSON.stringify() and JSON.parse().
const originalSchedule = {
year: 2023,
rates: {
federal: 0.22,
state: 0.05
},
effectiveDate: new Date('2023-01-01')
};
const clonedSchedule = JSON.parse(JSON.stringify(originalSchedule));
// Modify the clone
clonedSchedule.rates.federal = 0.25;
console.log(originalSchedule.rates.federal); // Outputs: 0.22 (unaffected)
console.log(clonedSchedule.rates.federal); // Outputs: 0.25
This method works for simple, data-only objects. It serializes the object to a JSON string and then parses it back into a completely new object. However, it comes with significant limitations.
Warning: The
JSON.stringify/JSON.parsemethod cannot handle complex data types. It will drop functions,undefinedvalues, and convertDateobjects into ISO 8601 date strings. Use it with caution.
The Modern Solution: structuredClone
Recognizing the need for a reliable deep-cloning mechanism, JavaScript introduced the global structuredClone() function. It uses the structured clone algorithm, which is a more robust and powerful way to create deep copies of objects.
const originalSchedule = {
year: 2023,
rates: {
federal: 0.22,
state: 0.05
},
effectiveDate: new Date('2023-01-01')
};
const clonedSchedule = structuredClone(originalSchedule);
// Modify the clone
clonedSchedule.rates.federal = 0.25;
console.log(originalSchedule.rates.federal); // Outputs: 0.22
console.log(originalSchedule.effectiveDate.getFullYear()); // Outputs: 2023
console.log(clonedSchedule.rates.federal); // Outputs: 0.25
console.log(clonedSchedule.effectiveDate.getFullYear()); // Outputs: 2023
Notice how the Date object was correctly cloned, not converted to a string. structuredClone can handle many complex types that the JSON method cannot, including Map, Set, Blob, File, and more. It is now the standard way to perform a deep copy in JavaScript.
The Principle of Immutability
While knowing how to clone objects is important, it's often better to adopt a programming pattern that avoids accidental mutations altogether. This is the principle of immutability: treating data as if it cannot be changed after it's created. Instead of modifying an object, you create a new one with the updated values.
In frameworks such as Angular and React, we'll actually get a performance boost by using immutable data structures.
Working with immutable data makes your application's state more predictable. Since you know data doesn't change in place, you can avoid a whole class of bugs related to shared references. This is why libraries like Redux and frameworks like React heavily favor immutable patterns. Instead of directly changing a state object, you create a new one based on the old one.
For complex state updates, creating deep copies manually can become tedious. This is where libraries like Immer come in handy. They use a clever technique called to let you write code that looks like you're mutating an object, but under the hood, Immer is safely creating a new, updated copy for you.
import { produce } from 'immer';
const baseState = {
user: { name: 'Alex', preferences: { theme: 'dark' } },
posts: [ { id: 1, title: 'Hello World' } ]
};
const nextState = produce(baseState, draftState => {
// This looks like a direct mutation, but Immer handles it safely.
draftState.user.preferences.theme = 'light';
});
console.log(baseState.user.preferences.theme); // 'dark'
console.log(nextState.user.preferences.theme); // 'light'
This approach gives you the best of both worlds: the safety of immutability with the simple, direct syntax of mutable code.