Introduction
If you've ever worked with objects or arrays in JavaScript, you might have run into unexpected mutations. You modify what you thought was a copy, only to find that the original data has changed too. This behavior can lead to subtle bugs that are hard to track down.
The root cause of this problem lies in how JavaScript handles references to complex data types. When you work with objects and arrays, you're often working with references rather than actual copies of the data. Understanding the difference between shallow and deep copying is crucial for writing predictable, bug-free code.
In this guide, we'll explore what shallow and deep copies are, how to create them, when to use each approach, and common pitfalls to avoid when copying data structures in JavaScript.
Methods of Copying in JavaScript?
In JavaScript, there are two main approaches to copying objects and arrays:
Shallow Copy creates a new object or array, but only copies the top-level properties. Any nested objects or arrays inside it are still referenced rather than copied. This means that if you modify a nested structure in the copied version, it also affects the original.
Deep Copy creates an entirely new object or array by recursively copying all nested structures. This results in a fully independent clone where changes in the copied structure do not affect the original at all.
Shallow Copy Techniques
A shallow copy creates a new object or array at the top level, but nested objects or arrays remain as references to the original data. Let's explore the different ways to create shallow copies.
Shallow Copying Objects
There are two primary methods for shallow copying objects:
Using the Spread Operator (...
)
The spread operator is the most modern and concise way to create a shallow copy of an object:
const originalUser = {
name: "John",
age: 30,
address: {
city: "New York",
zip: "10001"
}
};
const shallowCopy = { ...originalUser };
// Modifying top-level property (safe)
shallowCopy.name = "Bob";
console.log(originalUser.name); // "John" - unchanged
// Modifying nested property (affects original!)
shallowCopy.address.city = "Los Angeles";
console.log(originalUser.address.city); // "Los Angeles" - changed!
Using Object.assign()
The Object.assign()
method copies all enumerable properties from one or more source objects to a target object:
const originalUser = {
name: "John",
age: 30,
address: {
city: "New York",
zip: "10001"
}
};
const shallowCopy = Object.assign({}, originalUser);
// Same behavior as spread operator
shallowCopy.age = 35; // Safe - only affects copy
shallowCopy.address.zip = "90001"; // Affects original!
Shallow Copying Arrays
Arrays can also be shallow copied using several methods:
Using the Spread Operator (...
)
const originalArray = [1, 2, [3, 4]];
const shallowCopy = [...originalArray];
// Modifying top-level element (safe)
shallowCopy[0] = 99;
console.log(originalArray[0]); // 1 - unchanged
// Modifying nested array (affects original!)
shallowCopy[2][0] = 99;
console.log(originalArray[2][0]); // 99 - changed!
Using slice()
Method
const originalArray = [1, 2, [3, 4]];
const shallowCopy = originalArray.slice();
// Same shallow copy behavior
Using concat()
Method
const originalArray = [1, 2, [3, 4]];
const shallowCopy = [].concat(originalArray);
// Same shallow copy behavior
Deep Copy Techniques
A deep copy recursively copies all nested structures, creating a fully independent clone. Let's explore the methods available for deep copying.
Using structuredClone()
The structuredClone()
method is a modern, built-in way to create deep copies. It works with both objects and arrays and handles most data types correctly:
const originalUser = {
name: "John",
age: 30,
address: {
city: "New York",
zip: "10001"
},
hobbies: ["reading", "gaming"]
};
const deepCopy = structuredClone(originalUser);
// Modifying nested properties (safe)
deepCopy.address.city = "Los Angeles";
deepCopy.hobbies.push("cooking");
console.log(originalUser.address.city); // "New York" - unchanged
console.log(originalUser.hobbies); // ["reading", "gaming"] - unchanged
structuredClone()
also works seamlessly with arrays:
const originalArray = [1, 2, { name: "John" }, [3, 4]];
const deepCopy = structuredClone(originalArray);
deepCopy[2].name = "Bob";
deepCopy[3][0] = 99;
console.log(originalArray[2].name); // "John" - unchanged
console.log(originalArray[3][0]); // 3 - unchanged
Using Lodash _.cloneDeep()
If you need to support older browsers or have more complex cloning requirements, Lodash's _.cloneDeep()
method is a reliable third-party solution:
// First, install lodash with npm install lodash before importing
import _ from 'lodash';
const originalData = {
name: "John",
settings: {
theme: "dark",
notifications: {
email: true,
push: false
}
}
};
const deepCopy = _.cloneDeep(originalData);
deepCopy.settings.notifications.email = false;
console.log(originalData.settings.notifications.email); // true - unchanged
When to Use Each Approach
Choosing between shallow and deep copy depends on your data structure and use case:
Use Shallow Copy when:
- You only need to copy the first level of an object or array
- Your data structure doesn't contain nested objects or arrays
- You want better performance (shallow copies are faster)
- You're working with simple data structures like configuration objects with primitive values
Use Deep Copy when:
You're working with nested objects or arrays
You need complete independence from the original data
You're implementing features like undo/redo functionality
You're managing state in applications where mutations could cause bugs
You need to clone complex data structures without side effects
Common Pitfalls to Avoid
Even experienced developers can fall into traps when copying objects and arrays. Here are some common pitfalls and how to avoid them.
(1) Using Assignment Instead of Copying
The most common mistake is thinking that assignment (=
) creates a copy:
const original = { name: "John", age: 30 };
const copy = original; // This is NOT a copy!
copy.name = "Bob";
console.log(original.name); // "Bob" - both variables point to the same object!
Fix: Always use proper copying methods like spread operator or Object.assign()
:
const original = { name: "John", age: 30 };
const copy = { ...original }; // Now it's a real copy
copy.name = "Bob";
console.log(original.name); // "John" - unchanged
(2) Using Shallow Copy for Nested Structures
Using shallow copy methods when you actually need a deep copy leads to unexpected mutations:
const user = {
name: "John",
preferences: {
theme: "dark",
language: "en"
}
};
const updatedUser = { ...user }; // Shallow copy
updatedUser.preferences.theme = "light"; // Oops! Affects original
console.log(user.preferences.theme); // "light" - unexpected change!
Fix: Use structuredClone()
or _.cloneDeep()
for nested structures:
const user = {
name: "John",
preferences: {
theme: "dark",
language: "en"
}
};
const updatedUser = structuredClone(user); // Deep copy
updatedUser.preferences.theme = "light";
console.log(user.preferences.theme); // "dark" - original unchanged
(3) Limitations of structuredClone()
While structuredClone()
is powerful, it has limitations. It cannot clone:
- Functions
- DOM nodes
- Symbols
- Objects with getters/setters
const obj = {
name: "John",
greet: function() { return "Hello"; }, // Function won't be cloned
symbol: Symbol("id") // Symbol won't be cloned
};
const clone = structuredClone(obj);
console.log(clone.greet); // undefined - function was not cloned
Fix: For objects with functions, use _.cloneDeep()
from Lodash or implement custom cloning logic.
(4) Using JSON.parse(JSON.stringify())
for Deep Copying
Some developers use JSON.parse(JSON.stringify(obj))
as a quick deep copy solution, but this approach has significant drawbacks:
const obj = {
name: "John",
date: new Date(),
greet: function() { return "Hello"; },
undef: undefined
};
const clone = JSON.parse(JSON.stringify(obj));
console.log(clone.date); // String, not a Date object
console.log(clone.greet); // undefined - function was lost
console.log(clone.undef); // undefined - property was removed
This method fails with:
- Functions (completely removed)
-
undefined
values (removed) -
Date
objects (converted to strings) -
Infinity
andNaN
(converted tonull
) - Circular references (throws error)
Fix: Use structuredClone()
or _.cloneDeep()
instead for reliable deep copying.
Conclusion
Understanding the difference between shallow and deep copying is essential for writing reliable JavaScript code. While shallow copies are sufficient for simple, flat data structures, deep copies are necessary when working with nested objects and arrays to ensure complete independence from the original data.
The key takeaways are:
- Shallow copies only duplicate the top level, leaving nested structures as references to the original data
- Deep copies recursively duplicate all levels, creating fully independent clones
- Use
structuredClone()
for modern deep copying needs, or_.cloneDeep()
for more complex scenarios - Always be aware of which copying method you're using and whether it matches your data structure
By mastering these copying techniques, you'll avoid common bugs related to unexpected mutations and write more predictable, maintainable code. Whether you're managing application state, implementing undo functionality, or simply working with complex data structures, knowing when and how to properly copy your data is a fundamental skill that will serve you throughout your JavaScript journey.
Top comments (0)