How to Create a Deep Copy of an Object in JavaScript

How to Create a Deep Copy of an Object in JavaScript

Copying objects in JavaScript can be trickier than it seems, especially when dealing with deep copy. While shallow copies are simple, creating a complete, independent copy of an object, including all nested objects, takes more effort. This blog post discusses the complexities of deep copying in JavaScript, discussing different approaches and their applications.

Introduction to Object Copying

Before getting started with deep copying, you should first understand what it means to copy an object. When you clone an object, you create a new object with identical characteristics and values to the original. However, there is a difference between shallow and deep copying.

  • Shallow Copy: Creates a new object but merely replicates the references to nested objects.
  • Deep Copy: Creates a new object and recursively duplicates all nesting objects, ensuring there are no shared references.

Understanding these notions is critical for good object management in JavaScript.

Shallow Copying

Using Object.assign():

The Object.assign() method can make a shallow clone of an object.

const original = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, original);

console.log(shallowCopy); // { a: 1, b: { c: 2 } }
JavaScript

While this copies top-level properties, it just copies the reference to nested objects. Changes to nested objects will affect both the original and the copy.

Using the Spread Operator:

Another method for making a shallow copy is to use the spread operator ().

const shallowCopy = { ...original };

console.log(shallowCopy); // { a: 1, b: { c: 2 } }
JavaScript

Similar to Object.assign(), this method copies only the top-level properties and references nested objects.

Deep Copying

Deep copying guarantees perfect independence between the original and the copy. Here are a few ways to accomplish this in JavaScript.

Using JSON.stringify() and JSON.parse():

To build a deep copy, use JSON.stringify() and JSON.parse().

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

console.log(deepCopy); // { a: 1, b: { c: 2 } }
JavaScript

While this procedure is simple and successful, there are limitations:

  • It does not replicate functions.
  • It does not support undefined, NaN, Infinity, or specialized object types such as Date or RegExp.
Using a Recursive Function:

A custom recursive function can handle multiple data types while ensuring a deep copy.

function deepCopy(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  if (Array.isArray(obj)) {
    const arrCopy = [];
    for (const item of obj) {
      arrCopy.push(deepCopy(item));
    }
    return arrCopy;
  }

  const objCopy = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      objCopy[key] = deepCopy(obj[key]);
    }
  }

  return objCopy;
}

const deepCopiedObject = deepCopy(original);
console.log(deepCopiedObject); // { a: 1, b: { c: 2 } }
JavaScript
Using Lodash:

Lodash, a famous utility library, offers a dependable cloneDeep method.

const _ = require('lodash');

const deepCopiedObject = _.cloneDeep(original);
console.log(deepCopiedObject); // { a: 1, b: { c: 2 } }
JavaScript

Using Lodash makes deep copying easier and provides compatibility with multiple data types.

Handling Special Cases:

Deep copying can be difficult when dealing with unique cases such as circular references, Date objects, or functions.

1. Circular References

Circular references are when an object refers to itself, either directly or indirectly. Here’s an example.

const obj = { a: 1 };
obj.b = obj;

const deepCopy = JSON.parse(JSON.stringify(obj)); // Throws error
JavaScript

To manage circular references, you can use a custom function using a WeakMap:

function deepCopyCircular(obj, map = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  if (map.has(obj)) {
    return map.get(obj);
  }

  const copy = Array.isArray(obj) ? [] : {};
  map.set(obj, copy);

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepCopyCircular(obj[key], map);
    }
  }

  return copy;
}

const circularCopy = deepCopyCircular(obj);
console.log(circularCopy); // { a: 1, b: [Circular] }
JavaScript

2. Date Objects

Date objects require particular processing to ensure precise copying:

function deepCopyWithDates(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  if (obj instanceof Date) {
    return new Date(obj);
  }

  if (Array.isArray(obj)) {
    return obj.map(item => deepCopyWithDates(item));
  }

  const copy = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepCopyWithDates(obj[key]);
    }
  }

  return copy;
}

const dateObj = { date: new Date() };
const dateCopy = deepCopyWithDates(dateObj);
console.log(dateCopy); // { date: [Date] }
JavaScript

3. Functions

Copying functions typically entails copying their references, as their context cannot be duplicated:

const funcObj = { func: function() { console.log('Hello!'); } };
const funcCopy = deepCopy(funcObj);

console.log(funcCopy); // { func: [Function: func] }
funcCopy.func(); // Hello!
JavaScript

Performance Considerations

Deep copying can be resource costly, particularly for large objects. Performance can differ significantly between techniques.

  • JSON.stringify() and JSON.parse(): Fast for small, simple objects, but have a limited scope.
  • Recursive Function: Flexible, but may be slower for highly nested structures.
  • Lodash: Performance-optimized but introduces an external dependence.

Benchmarking

To discover the optimal method for your use case, consider benchmarking various approaches:

const { performance } = require('perf_hooks');

const largeObject = { /* a large, complex object */ };

const start = performance.now();
const deepCopy1 = JSON.parse(JSON.stringify(largeObject));
const end = performance.now();
console.log(`JSON method: ${end - start}ms`);

const start2 = performance.now();
const deepCopy2 = deepCopy(largeObject);
const end2 = performance.now();
console.log(`Recursive method: ${end2 - start2}ms`);

const start3 = performance.now();
const deepCopy3 = _.cloneDeep(largeObject);
const end3 = performance.now();
console.log(`Lodash method: ${end3 - start3}ms`);
JavaScript

By comparing performance, you may choose which method best balances speed and accuracy for your needs.

Conclusion

Creating a deep copy of a JavaScript object is critical for maintaining data integrity and preventing unwanted consequences. While there are several strategies to accomplish this, each has pros and disadvantages. Understanding the context and requirements of your application will assist you in selecting the best method.

  • For simple objects: JSON.stringify() and JSON.parse() offer a quick and straightforward approach.
  • For complex objects: Custom recursive functions or utility libraries, such as Lodash, provide greater flexibility and reliability.
  • For special cases: Handling circular references, date objects, and functions necessitates custom solutions.

By understanding these strategies, you can ensure that your JavaScript projects contain solid and maintainable code.

FAQ

What is the difference between a shallow copy and a deep copy in JavaScript?

A shallow copy merely duplicates top-level properties and references to nested objects, whereas a deep copy generates totally independent copies of all nested objects.

Why can’t I use JSON.stringify() and JSON.parse() for all deep copy operations?

These methods can’t handle functions, undefined, NaN, Infinity, Date, RegExp, or circular references, rendering them unsuitable for complicated objects.

How can I handle circular references when deep copying objects?

To track and manage circular references, create a custom recursive function with a WeakMap, or use a library that manages them automatically, such as Lodash.

Is using a library like Lodash necessary for deep copying objects?

Although not required, Lodash simplifies deep copying and handles a wide range of data types effectively. You can utilize it for dependability or create custom logic to eliminate dependencies.

How can I optimize deep copying for large objects in JavaScript?

To discover the most performant solution for your use case, employ efficient recursive functions, reduce excessive copying, and apply benchmarking techniques.

Have questions about this blog? Contact us for assistance!