Shallow Copies Causing Unintended Mutations in JavaScript: A Complete Guide
π Introduction
In JavaScript, when working with objects and arrays, making a shallow copy instead of a deep copy can lead to unintended mutations. This means that modifying the copied object might also change the original object, leading to unexpected behavior and bugs.
This guide will cover:
- What is a shallow copy?
- How shallow copies cause unintended mutations?
- Examples of shallow copies causing issues
- How to create deep copies to prevent mutations?
- Best practices and performance considerations
1οΈβ£ What is a Shallow Copy?
A shallow copy creates a new object or array but only copies the top-level properties. If the original object contains nested objects or arrays, the shallow copy still references the same nested structures.
πΉ Example of a Shallow Copy
let original = { name: "Alice", details: { age: 25, city: "New York" } };
let copy = { ...original }; // Shallow copy using spread operator
copy.details.age = 30; // Modifying nested property
console.log(original.details.age); // 30 (original object is also changed!)
console.log(copy.details.age); // 30
π΄ Problem: Changing copy.details.age
also changes original.details.age
because details
is still referencing the same object.
2οΈβ£ How Shallow Copies Cause Unintended Mutations?
Since a shallow copy only duplicates top-level properties, nested objects or arrays remain linked to the original. When you modify the nested values in the copied object, it affects the original as well.
πΉ Shallow Copies in Arrays
let arr1 = [[1, 2], [3, 4]];
let arr2 = [...arr1]; // Shallow copy
arr2[0][0] = 99; // Modify nested array
console.log(arr1[0][0]); // 99 (Original array is also modified!)
π΄ Issue: The inner arrays [1, 2]
and [3, 4]
are still shared between arr1
and arr2
.
3οΈβ£ Examples of Shallow Copies Causing Issues
πΉ Example 1: Using Object.assign()
let obj1 = { x: 10, y: { z: 20 } };
let obj2 = Object.assign({}, obj1); // Shallow copy
obj2.y.z = 99;
console.log(obj1.y.z); // 99 (Original object is affected!)
π΄ Issue: Object.assign()
only copies top-level properties.
β Solution: Use structured cloning or deep copy techniques.
πΉ Example 2: Spread Operator (...
) with Objects
let person = { name: "Bob", address: { city: "London", zip: "12345" } };
let copiedPerson = { ...person }; // Shallow copy
copiedPerson.address.city = "Paris";
console.log(person.address.city); // "Paris" (original object is changed)
π΄ Issue: The address
property in copiedPerson
still points to the same object in person
.
πΉ Example 3: Array.prototype.slice()
Creates a Shallow Copy
let numbers = [[1, 2], [3, 4]];
let numbersCopy = numbers.slice(); // Shallow copy
numbersCopy[1][0] = 100;
console.log(numbers[1][0]); // 100 (Original array is affected)
π΄ Issue: The nested arrays remain linked.
4οΈβ£ How to Create Deep Copies to Prevent Mutations?
To avoid unintended mutations, use deep copy techniques instead of shallow copies.
β Method 1: JSON Methods (Simple but Limited)
let deepCopy = JSON.parse(JSON.stringify(original));
βοΈ Pros: Easy to use, removes references
β Cons: Doesnβt work for undefined
, functions, or circular references.
β Method 2: Recursive Deep Copy Function
function deepClone(obj) {
if (obj === null || typeof obj !== "object") return obj;
let clone = Array.isArray(obj) ? [] : {};
for (let key in obj) {
clone[key] = deepClone(obj[key]);
}
return clone;
}
let objCopy = deepClone(original);
βοΈ Pros: Handles complex objects
β Cons: Can be slow for large structures
β
Method 3: Using structuredClone()
(Best Native Approach)
let deepCopy = structuredClone(original);
βοΈ Pros: Fast, native, supports undefined
and Map/Set
β Cons: Not supported in older browsers
β
Method 4: Lodash _.cloneDeep()
import _ from "lodash";
let deepCopy = _.cloneDeep(original);
βοΈ Pros: Battle-tested, handles all edge cases
β Cons: Requires an external library
5οΈβ£ Best Practices and Performance Considerations
- Use shallow copies (
{ ...obj }
orslice()
) for flat structures (e.g.,{ name: "Alice", age: 25 }
). - Use deep copies (
JSON.parse(JSON.stringify(obj))
) for simple nested objects (but be aware of its limitations). - Use
structuredClone()
if working with modern browsers. - Use Lodash
_.cloneDeep()
when dealing with complex objects. - Avoid modifying objects directly to prevent unintended side effects.
6οΈβ£ Real-World Example
Imagine a React application where state is updated incorrectly due to a shallow copy:
const [user, setUser] = useState({ name: "Alice", details: { age: 25, city: "New York" } });
const updateAge = () => {
let newUser = { ...user }; // Shallow copy
newUser.details.age = 30;
setUser(newUser);
};
updateAge();
console.log(user.details.age); // 30 (Unintended mutation)
π΄ Issue: React does not detect changes when only nested properties are modified.
β Solution: Use deep copies:
const updateAge = () => {
let newUser = structuredClone(user); // Deep copy
newUser.details.age = 30;
setUser(newUser);
};
π― Conclusion
- Shallow copies (
{ ...obj }
,slice()
,Object.assign()
) only copy top-level properties, leading to unintended mutations. - Nested objects/arrays remain linked in shallow copies.
- Deep copies (
structuredClone()
, Lodash_.cloneDeep()
) are safer when working with complex structures. - Using proper cloning techniques prevents hard-to-debug issues.
Would you like a specific performance comparison between these methods?