Shallow Copy vs Deep Copy: Understanding the Differences and Use CasesUnderstanding Object Copying: Shallow vs Deep Copy

Introduction: Object Copying in Programming

In modern web development, particularly in JavaScript and TypeScript, copying objects is a common operation. Whether you're working with complex data structures or passing objects between functions, understanding the difference between shallow copy and deep copy is critical. These two methods of copying objects have distinct behaviors that can significantly impact your application’s performance, memory usage, and data integrity.

Object copying, in its simplest form, involves creating a new object that is a copy of an existing one. However, the depth of this copy determines whether changes to the original object affect the new one, and vice versa. In this post, we will explore the differences between shallow copy and deep copy, their respective use cases, and how you can implement them effectively in JavaScript.

Shallow Copy: The Basics

A shallow copy creates a new object, but it only copies the reference of nested objects, not the actual objects themselves. This means that if the copied object contains references to other objects, those references will point to the same objects in both the original and copied versions. As a result, changes to nested objects in one will reflect in the other.

Examples:

const originalObject = {
  a: 1,
  b: 2,
  c: { d: 3 },
};

const shallowCopy = { ...originalObject };

shallowCopy.c.d = 4;

console.log(originalObject.c.d); // 4
console.log(shallowCopy.c.d); // 4
const original = { name: "John", address: { city: "New York" } };
const shallowCopy = { ...original };

shallowCopy.name = "Jane"; // Changing a primitive field
shallowCopy.address.city = "San Francisco"; // Changing a nested object

console.log(original.name); // Output: John (No change)
console.log(original.address.city); // Output: San Francisco (Changed)

In the above example, while the change to the name field in the shallowCopy object does not affect the original, the change to the nested address.city field does. This happens because the shallow copy only copies the reference to the address object, not its value.

Use Cases for Shallow Copy:

  • The original object does not contain nested objects.

  • You want to create a new object with the same properties as the original object, but do not need to duplicate the nested objects.

  • When working with objects that contain mostly primitive values.

  • In cases where you need a lightweight copy of an object for quick manipulations.

  • When working with large objects and performance is a concern, and you are okay with the risks associated with shared references.

Deep Copy: The Complete Picture

A deep copy, on the other hand, duplicates not only the original object but also the objects nested within it. This ensures that the copied object and the original object are completely independent. Changes made to one will not affect the other, even in deeply nested structures.

Example:

const original = { name: "John", address: { city: "New York" } };

const deepCopy = JSON.parse(JSON.stringify(original));

deepCopy.name = "Jane";
deepCopy.address.city = "San Francisco";

console.log(original.name); // Output: John (No change)
console.log(original.address.city); // Output: New York (No change)
const originalObject = {
  a: 1,
  b: 2,
  c: { d: 3 },
};

const deepCopy = JSON.parse(JSON.stringify(originalObject));

deepCopy.c.d = 4;

console.log(originalObject.c.d); // 3
console.log(deepCopy.c.d); // 4

In this example, changes made to the deepCopy object have no effect on the original object. This is because the deep copy creates a new instance of every object, including nested ones, thus maintaining complete independence.

Use Cases for Deep Copy:

  • When working with complex objects that contain nested structures.
  • In scenarios where data integrity is crucial, and unintended side effects should be avoided.
  • When handling objects in immutability-focused patterns, such as Redux in React.
  • You want to create a completely independent copy of the original object, including all nested objects.

When to Use Shallow Copy vs Deep Copy

Choosing between shallow and deep copy depends on the nature of the data you're working with and your application's specific requirements. If you're working with simple objects that do not have nested structures, a shallow copy is often more efficient and sufficient. It’s also the preferred approach when performance is a concern, as deep copying large, complex objects can be resource-intensive.

However, if your objects contain nested references (arrays, objects within objects), and you want to prevent changes in one from affecting the other, deep copy is the safer option. In frameworks like React, where immutability is often required to manage state effectively, deep copying ensures that components re-render only when actual changes occur in state or props.

Shallow Copy Use Cases:

  • Handling objects with primitive data types.
  • When performance and memory usage are top priorities, and shared references are acceptable.

Deep Copy Use Cases:

  • Managing large, deeply nested objects that require independent copies.
  • Scenarios requiring strict immutability, such as in Redux state management.

Pros and Cons

Shallow Copy

Pros:

  • Faster execution time.

  • Less memory usage. Cons:

  • Not suitable for objects with nested objects, as changes to nested objects in the copy will affect the original object.

Deep Copy

Pros:

  • Creates a completely independent copy of the original object, including all nested objects.

  • Prevents unintended side effects or data manipulation when working with complex data structures. Cons:

  • Slower execution time, especially for large or complex objects.

  • Increased memory usage.

Deep Copy vs Shallow Copy in TypeScript

Linear or Non-linear Data Structures?

Deep and shallow copies are operations that can be applied to both linear (arrays, linked lists) and non-linear data structures (objects, trees, graphs). The categorization of the data structure is independent of the copy method used. The key distinction lies in how the data is copied, not the structure itself.

  • Shallow Copy: Copies references to the objects, not the objects themselves. If the original data changes, the shallow copy may also reflect those changes.
  • Deep Copy: Copies the actual values of objects and sub-objects, ensuring no reference is shared between the original and the copied version.

Implementation Techniques for Shallow and Deep Copy

In JavaScript, there are several ways to implement shallow and deep copy depending on your specific needs.

Shallow Copy Techniques:

Spread Operator (...)

  • Type: Shallow copy.
  • Usage: The spread operator creates a new object or array by spreading the properties or elements of an existing one.
  • Example:
    const obj1 = { a: 1, b: { c: 2 } };
    const copy = { ...obj1 };
    copy.b.c = 3; // Again, changes `obj1.b.c` as well
    
  • Pitfall: Like Object.assign(), it only creates a shallow copy. Nested objects or arrays are still references.

Object.assign()

  • Type: Shallow copy.
  • Usage: Copies enumerable properties from one or more source objects to a target object.
  • Example:
    const obj1 = { a: 1, b: { c: 2 } };
    const copy = Object.assign({}, obj1);
    copy.b.c = 3; // Changes the nested property in both `obj1` and `copy`
    
  • Pitfall: It performs a shallow copy, so any nested objects are still references to the original object.

Deep Copy Techniques:

JSON.parse(JSON.stringify())

  • Type: Deep copy.
  • Usage: Converts an object into a JSON string and then parses it back into a new object.
  • Example:
    const obj1 = { a: 1, b: { c: 2 } };
    const deepCopy = JSON.parse(JSON.stringify(obj1));
    deepCopy.b.c = 3; // `obj1` remains unchanged
    
  • Pitfall:
    • Loss of methods: This method only works with simple data types (no functions or non-JSON-serializable types like undefined, Date, Map, Set, etc.).
    • Performance: Converting to a string and back can be inefficient for large or complex objects.

structuredClone()

  • Type: Deep copy.
  • Usage: A native browser API that performs a deep clone on an object, including handling many non-JSON-serializable types like Date, RegExp, ArrayBuffer, and more.
  • Example:
    const obj1 = { a: 1, b: { c: 2, d: new Date() } };
    const deepCopy = structuredClone(obj1);
    deepCopy.b.c = 3; // `obj1` remains unchanged
    
  • Pitfall: This is still a relatively new API and may not be available in all environments (older browsers or certain versions of Node.js).

Custom Recursive Functions

  • Type: Deep copy.

  • Usage: For more complex objects or objects containing non-serializable types, writing a custom deep copy function may be required. For more complex objects or objects containing non-serializable types, writing a custom deep copy function may be required.

  • Example:

    function deepCopy(obj) {
      if (obj === null || typeof obj !== "object") return obj;
    
      const copy = Array.isArray(obj) ? [] : {};
    
      for (let key in obj) {
        copy[key] = deepCopy(obj[key]);
      }
      return copy;
    }
    
  • Pitfall: Custom deep copy functions can be complex to implement and may not handle all edge cases.

3. Best Practices

  • Shallow Copy Use Cases: Use shallow copies when working with simple objects that don't contain nested structures or if you only need to copy the top level.
    • Prefer the spread operator for its simplicity and readability.
    • Object.assign() can be useful for merging multiple objects into one.
  • Deep Copy Use Cases: Use deep copies when dealing with nested objects or when you want to ensure that changes to the copy don't affect the original object.
    • If working with simple, serializable objects, JSON.parse(JSON.stringify()) is still a quick option.
    • For more complex objects (with non-JSON-serializable data types), prefer structuredClone() if it's available in your environment.

4. Pitfalls

  • Performance Considerations: Deep copies can be slow and memory-intensive, especially for large or complex objects. If performance is a concern, evaluate whether a deep copy is actually necessary.

  • Unexpected Shared References: With shallow copies, it's easy to accidentally mutate the original object due to shared references for nested properties. Be mindful of how your objects are structured and when a deep copy is required.

  • Unsupported Data: JSON.parse(JSON.stringify()) does not work with objects containing methods, Date objects, Map, Set, undefined, and other non-serializable types. In such cases, this method can silently lose data, leading to hard-to-find bugs.

Summary

| Method | Copy Type | Supports Nested Objects | Serializes Special Types (Date, Map, etc.) | Performance | | ------------------------------ | --------- | ----------------------- | ------------------------------------------ | ----------- | | Object.assign() | Shallow | No | No | Fast | | Spread Operator (...) | Shallow | No | No | Fast | | JSON.parse(JSON.stringify()) | Deep | Yes | No | Slow | | structuredClone() | Deep | Yes | Yes | Moderate |

Conclusion: Choosing the Right Approach

Understanding the differences between shallow and deep copying is fundamental for efficient memory management and ensuring data integrity in your applications. While shallow copies are faster and sufficient for simple, non-nested objects, deep copies provide independence and protection against unintended changes when dealing with complex, nested structures.

In web development, particularly in frameworks like React and libraries like Redux, where immutability and efficient state management are essential, knowing when and how to use shallow vs deep copies can save you from hard-to-diagnose bugs and performance pitfalls. Always consider the specific needs of your project and the data structures you're working with before choosing the appropriate copy method.