Shallow vs. Deep Copying in JavaScript
Copying and modifying objects in JavaScript is never as simple as it seems. Understanding how objects and references work during this process is essential for web developers and can save hours of debugging. This becomes increasingly important when you work with large stateful applications like those built in React or Vue.
Shallow copying and deep copying refer to how we make copies of an object in JavaScript and what data is created in the ‘copy’. In this article, we’ll delve into the distinctions between these methods, explore their real-world applications, and uncover the potential pitfalls that can emerge when using them.
What is ‘Shallow’ Copying
Shallow copying refers to the process of creating a new object that is a copy of an existing object, with its properties referencing the same values or objects as the original. In JavaScript, this is often achieved using methods like Object.assign()
or the spread syntax ({...originalObject}
). Shallow copying only creates a new reference to the existing objects or values and doesn’t create a deep copy, which means that nested objects are still referenced, not duplicated.
Let’s look at the following code example. The newly created object shallowCopyZoo
is created as a copy of zoo
via the spread operator, which has caused some unintended consequences.
let zoo = {
name: "Amazing Zoo",
location: "Melbourne, Australia",
animals: [
{
species: "Lion",
favoriteTreat: "🥩",
},
{
species: "Panda",
favoriteTreat: "🎋",
},
],
};
let shallowCopyZoo = { ...zoo };
shallowCopyZoo.animals[0].favoriteTreat = "🍖";
console.log(zoo.animals[0].favoriteTreat);
// "🍖", not "🥩"
But let’s look at what is really in shallowCopyZoo
. The properties name
and location
are primitive values (string), so their values are copied. However, the animals
property is an array of objects, so the reference to that array is copied, not the array itself.
You can quickly test this (if you don’t believe me) using the strict equality operator (===
). An object is only equal to another object if the refer to the same object (see Primitive vs. Reference data types). Notice how the property animals
is equal on both but the objects themselves are not equal.
console.log(zoo.animals === shallowCopyZoo.animals)
// true
console.log(zoo === shallowCopyZoo)
// false
This can lead to potential issues in code bases and make life especially hard when working with large Modifying a nested object in the shallow copy also affects the original object and any other shallow copies, as they all share the same reference.
Deep Copying
Deep copying is a technique that creates a new object, which is an exact copy of an existing object. This includes copying all its properties and any nested objects, instead of references. Deep cloning is helpful when you need two separate objects that don’t share references, ensuring changes to one object don’t affect the other.
Programmers often use deep cloning when working with application state objects in complex applications. Creating a new state object without affecting the previous state is crucial for maintaining the application’s stability and implementing undo-redo functionality properly.
How to deep copy using JSON.stringify() and JSON.parse()
A popular and library-free way of deep copying is to use the built in JSON stringify()
and parse()
methods.
The parse(stringify()) method is not perfect. For example, special data types like Date
will be stringified and undefined
values will be ignored. Like all options in this article, it should be considered for your individual use case.
In the code below, we’ll create a deepCopy
function these methods to deep clone an object. We then copy the playerProfile
object and modify the copied object without affecting the original one. This showcases the value of deep copying in maintaining separate objects without shared references.
const playerProfile = {
name: 'Alice',
level: 10,
achievements: [
{
title: 'Fast Learner',
emoji: '🚀'
},
{
title: 'Treasure Hunter',
emoji: '💰'
}
]
};
function deepCopy(obj) {
return JSON.parse(JSON.stringify(obj));
}
const clonedProfile = deepCopy(playerProfile);
console.log(clonedProfile);
/* Output:
{
name: 'Alice',
level: 10,
achievements: [
{
title: 'Fast Learner',
emoji: '🚀'
},
{
title: 'Treasure Hunter',
emoji: '💰'
}
]
}
*/
// Modify the cloned profile without affecting the original profile
clonedProfile.achievements.push({ title: 'Marathon Runner', emoji: '🏃' });
console.log(playerProfile.achievements.length); // Output: 2
console.log(clonedProfile.achievements.length); // Output: 3
Libraries for Deep Copying
There are also a variety of third-party libraries that offer a deep copying solution.
- The Lodash Library
cloneDeep()
function that handles circular references, functions, and special objects correctly. - The jQuery Library
extend()
[deep = true] function - The immer Library has been build with React-Redux developers in mind and provides handy tools for mutating objects.
A Vanilla JS Deep Copy Function
If for some reason you do not want to use the JSON object or a third party library, you can also create a custom deep copy function in vanilla JavaScript. that recursively iterates through the object properties and creates a new object with the same properties and values.
const deepCopy = (obj) => {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
const newObj = Array.isArray(obj) ? [] : {};
for (const key in obj) {
newObj[key] = deepCopy(obj[key]);
}
return newObj;
}
const deepCopiedObject = deepCopy(originalObject);
Downsides of Deep Copying
While deep copying offers great benefits for data accuracy, it’s recommended to evaluate whether deep copying is necessary for each specific use case. In some situations, shallow copying or other techniques for managing object references might be more suitable, providing better performance and reduced complexity.
- Performance impact: Deep copying can be computationally expensive, especially when dealing with large or complex objects. As the deep copy process iterates through all nested properties, it may take a significant amount of time, negatively impacting the performance of your application.
- Memory consumption: Creating a deep copy results in the duplication of the entire object hierarchy, including all nested objects. This can lead to increased memory usage, which may be problematic, particularly in memory-constrained environments or when dealing with large data sets.
- Circular references: Deep copying can cause issues when objects contain circular references (i.e., when an object has a property that refers back to itself, directly or indirectly). Circular references can lead to infinite loops or stack overflow errors during the deep copy process, and handling them requires additional logic to avoid these issues.
- Function and special object handling: Deep copying may not handle functions or objects with special characteristics (e.g., Date, RegExp, DOM elements) as expected. For example, when deep copying an object containing a function, the function’s reference might be copied, but the function’s closure and its bound context will not be duplicated. Similarly, objects with special characteristics might lose their unique properties and behavior when deep copied.
- Implementation complexity: Writing a custom deep copy function can be complex, and built-in methods like
JSON.parse(JSON.stringify(obj))
have limitations, such as not handling functions, circular references, or special objects correctly. While there are third-party libraries like Lodash’s_.cloneDeep()
that can handle deep copying more effectively, adding an external dependency for deep copying might not always be ideal.
Conclusion
Thanks for taking the time to read this article. Shallow vs. Deep copying is surprisingly more complex than any first timer imagines. Although there are a lot of pitfalls in each approach, taking the time to review and consider the options will ensure your application and data remains exactly how you want it to be.