Shallow copies causing unintended mutations

Loading

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:

  1. What is a shallow copy?
  2. How shallow copies cause unintended mutations?
  3. Examples of shallow copies causing issues
  4. How to create deep copies to prevent mutations?
  5. 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

  1. Use shallow copies ({ ...obj } or slice()) for flat structures (e.g., { name: "Alice", age: 25 }).
  2. Use deep copies (JSON.parse(JSON.stringify(obj))) for simple nested objects (but be aware of its limitations).
  3. Use structuredClone() if working with modern browsers.
  4. Use Lodash _.cloneDeep() when dealing with complex objects.
  5. 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?

Leave a Reply

Your email address will not be published. Required fields are marked *