My Logo

PUBLISHED JUNE 11, 2024

Teghen Donald Ticha
Lastly Updated: 3 months ago
Reading time 11 mins

How do you deep clone an object in JavaScript?

Learn how to deep clone objects in JavaScript with various methods including JSON, recursion, structured cloning, and Lodash, including best practices and pitfalls.
How do you deep clone an object in JavaScript?

Prerequisite

Before learning about deep cloning in JavaScript, you should have a basic understanding of:

  1. 1.JavaScript Objects: Creating and manipulating objects.
  2. 2.JavaScript Functions: Defining and invoking functions.
  3. 3.Data Types: Primitive (string, number) and reference (objects, arrays).
  4. 4.ES6 Features: Arrow functions, destructuring, and spread/rest operators.
  5. 5.JSON: Using JSON.parse and JSON.stringify.

These basics will help you grasp deep cloning concepts effectively.

Introduction

In JavaScript, cloning an object means creating a new object with the same properties and values as an existing object.

This is often necessary when you want to make changes to an object without affecting the original one.

Cloning can be useful in many scenarios, such as copying configuration objects, managing state in applications, and avoiding unintended side effects.


Difference Between Shallow and Deep Cloning

Cloning can be categorized into two types: shallow cloning and deep cloning.

  • Shallow Cloning: This creates a new object and copies the references of the original object's properties to the new object.
    If the properties are objects themselves, only the reference is copied, not the actual object.
    This means changes to nested objects in the cloned object will affect the original object.
const originalOb = { 
  a: 1, 
  b: { 
    c: 2 
  } 
};
const shallowCloneOb = Object.assign({}, originalOb);
shallowCloneOb.b.c = 3;

console.log(originalOb.b.c); // Output: 3


const original = {
  name: "yohan",
  age: 1,
  address: {
    city: "Douala",
    zip: "00000"
  }
};

// Perform shallow cloning using Object.assign
const shallowClone = Object.assign({}, original);

// Modify the nested object in the clone
shallowClone.address.city = "Alberta";

console.log(original.address.city); // Output: Alberta
console.log(shallowClone.address.city); // Output: Alberta

Just like in the first example, we bserve that changing the city property in the shallowClone also changes the city property in the original object because both address properties refer to the same object.

  • Deep Cloning: This creates a new object and recursively copies all nested objects and their properties.
    The new object is a completely independent copy of the original.
    Changes to the deep-cloned object do not affect the original object.
const originalOb = { 
  a: 1, 
  b: { 
    c: 2 
  } 
};
const deepCloneOb = JSON.parse(JSON.stringify(originalOb));;
deepCloneOb.b.c = 'c';

console.log(originalOb.b.c); // Output: 2


const original = {
  name: "yohan",
  age: 1,
  address: {
    city: "Douala",
    zip: "00000"
  }
};

// Perform shallow cloning using Object.assign
const deepClone = JSON.parse(JSON.stringify(original));

// Modify the nested object in the clone
deepClone.address.city = "Alberta";

console.log(original.address.city); // Output: Douala
console.log(deepClone.address.city); // Output: Alberta

Same result as in the previous example, changing the city property in the deepClone does not affect the city property in the original object because address in the deepClone is a completely new object, independent of the address in the original object.


Importance of Deep Cloning

Deep cloning is crucial when working with complex data structures where nested objects and arrays are involved.

It ensures that changes made to the cloned object do not impact the original object, maintaining data integrity and preventing side effects.

This is especially important in scenarios like:

  • State Management in Applications: When managing state, particularly in frameworks like `React` or `Vue`, deep cloning helps ensure that updates to state objects do not mutate the previous state unintentionally.
  • Immutable Data Structures: In functional programming paradigms, immutability is key. Deep cloning allows you to work with immutable data structures, ensuring that existing data remains unchanged.
  • Copying Complex Configurations: When working with configurations that contain nested objects or arrays, deep cloning ensures that modifications to the configuration do not affect the original settings.

1. JSON.parse and JSON.stringify

One of the simplest ways to perform a deep clone in JavaScript is by using the combination of JSON.parse and JSON.stringify.

This method converts an object into a JSON string and then parses the string back into a new object.

This approach effectively creates a deep copy of the original object.

Let's illustrate by revisit our example from above:

const original = {
  name: "Yohan",
  age: 1,
  address: {
    city: "Douala",
    zip: "00000"
  },
  hobbies: ["Playing", "Crying"],
  bornOn: new Date('2024-02-09')
};

// Deep clone using JSON methods
const deepClone = JSON.parse(JSON.stringify(original));

// Modify the nested object and array in the clone
deepClone.address.city = "Alberta";
deepClone.hobbies.push("Eating");

console.log(original.address.city); // Output: Wonderland
console.log(deepClone.address.city); // Output: Dreamland

console.log(original.hobbies); // Output: [ 'Playing', 'Crying' ]
console.log(deepClone.hobbies); // Output: [[ 'Playing', 'Crying', 'Eating' ]

console.log(original.bornOn, typeof original.bornOn); // Output:  2024-02-09T00:00:00.000Z object
console.log(deepClone.bornOn, typeof deepClone.bornOn); // Output:  2024-02-09T00:00:00.000Z string


Implications

  • Pros: Simple to use and understand.
  • Cons: Limited to JSON-compatible objects, can be slow for large objects.
  • Performance: Generally slower due to the serialization and deserialization process.


Limitations

While JSON.parse and JSON.stringify provide a simple way to deep clone objects, they have notable limitations:

  • Functions and Symbols: These methods do not handle functions or symbols, which are omitted during the cloning process.
  • Circular References: They cannot clone objects with circular references and will throw an error.
  • Special Data Types: This approach does not handle special data types like Date, Set, Map, RegExp, and undefined, as observed with the last two console.log statements.


2. Using Recursion

A more flexible approach to deep cloning involves writing a custom recursive function.

This method can handle many of the limitations of the JSON methods by explicitly checking and copying each property of the object, including nested objects.

function deepClone(obj, visited = new WeakMap()) {
  if (obj === null || typeof obj !== "object") {
    return obj;
  }

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

  let clone;

  if (Array.isArray(obj)) {
    clone = [];
    visited.set(obj, clone);
    obj.forEach((item, index) => {
      clone[index] = deepClone(item, visited);
    });
  } 
  else if (obj instanceof Date) {
    clone = new Date(obj);
  } 
  else if (obj instanceof RegExp) {
    clone = new RegExp(obj);
  } 
  else {
    clone = {};
    visited.set(obj, clone);
    Object.keys(obj).forEach((key) => {
      clone[key] = deepClone(obj[key], visited);
    });
  }

  return clone;
}


const original = {
  name: "Yohan",
  age: 1,
  address: {
    city: "Douala",
    zip: "00000"
  },
  hobbies: ["Crying", "Playing"],
  birthDate: new Date("2024-02-09"),
  pattern: /ab+c/
};

// Deep clone using custom recursive function
const deepCloneObj = deepClone(original);

// Modify the nested object and array in the clone
deepCloneObj.address.city = "Alberta";
deepCloneObj.hobbies.push("Eating");

console.log(original.address.city); // Output: Douala
console.log(deepCloneObj.address.city); // Output: Alberta

console.log(original.hobbies); // Output: [ 'Crying', 'Playing' ]
console.log(deepCloneObj.hobbies); // Output: [ 'Crying', 'Playing', 'Eating' ]

console.log(original.birthDate, typeof original.birthDate) // Outputs: 2024-02-09T00:00:00.000Z object
console.log(deepCloneObj.birthDate, typeof deepCloneObj.birthDate) // Outputs: 2024-02-09T00:00:00.000Z object



Implications

  • Pros: Highly customizable, can handle special data types.
  • Cons: Can be complex and error-prone, particularly with circular references.
  • Performance: Performance varies based on implementation; deep nested objects may result in stack overflow.



Benefit of Using a Custom Recursive Function

The custom recursive function above handles several edge cases such as:

  • Circular References: Managed using a WeakMap to track visited objects.
  • Special Data Types: Includes support for Date and RegExp.(as observed with last two console.log statements)



3. Structured Cloning Using structuredClone()

The structuredClone method, available in modern browsers, is a built-in method for creating deep clones of objects.

It is designed to handle many of the limitations of other methods, including support for functions, symbols, and special data types.

const original = {
  name: "Yohan",
  age: 1,
  address: {
    city: "Douala",
    zip: "12345"
  },
  hobbies: [ 'Crying', 'Playing' ],
  birthDate: new Date("2024-02-09"),
  pattern: /ab+c/
};

// Deep clone using structuredClone
const deepClone = structuredClone(original);

// Modify the nested object and array in the clone
deepClone.address.city = "Alberta";
deepClone.hobbies.push("Eating");

console.log(original.address.city); // Output: Douala
console.log(deepClone.address.city); // Output: Alberta

console.log(original.hobbies); // Output: [ 'Crying', 'Playing' ]
console.log(deepClone.hobbies); // Output: [ 'Crying', 'Playing', 'Eating' ]

console.log(original.birthDate, typeof original.birthDate); // Output: 2024-02-09T00:00:00.000Z object
console.log(deepClone.birthDate, typeof deepClone.birthDate); // Output: 2024-02-09T00:00:00.000Z object

console.log(original.pattern, typeof original.pattern); // Output: /ab+c/ object
console.log(deepClone.pattern, typeof deepClone.pattern); // Output: /ab+c/ object

This method is straightforward and highly efficient for deep cloning complex objects, making it a preferred choice when available.



Implications

  • Pros: Built-in support for many data types, handles complex structures well.
  • Cons: Limited to modern browsers and environments that support it.
  • Performance: Efficient for complex and large objects.



Using Third-Party Libraries

There are several third-party libraries available for deep cloning objects in JavaScript, each with its own advantages. Some popular ones include:

1. Lodash: a popular JavaScript utility library that provides a wide range of functions, including a robust cloneDeep function for deep cloning objects. This method is well-tested and handles many edge cases effectively.

const _ = require('lodash');

const original = {
  name: "Yohan",
  age: 1,
  address: {
    city: "Douala",
    zip: "12345"
  },
  hobbies: [ 'Crying', 'Playing' ],
  birthDate: new Date("2024-02-09"),
  pattern: /ab+c/
};

// Deep clone using Lodash's cloneDeep
const deepClone = _.cloneDeep(original);

// Modify the nested object and array in the clone
deepClone.address.city = "Alberta";
deepClone.hobbies.push("Eating");

console.log(original.address.city); // Output: Douala
console.log(deepClone.address.city); // Output: Alberta

console.log(original.hobbies); // Output: [ 'Crying', 'Playing' ]
console.log(deepClone.hobbies); // Output: [ 'Crying', 'Playing', 'Eating' ]

console.log(original.birthDate, typeof original.birthDate); // Output: 2024-02-09T00:00:00.000Z object
console.log(deepClone.birthDate, typeof deepClone.birthDate); // Output: 2024-02-09T00:00:00.000Z object

console.log(original.pattern, typeof original.pattern); // Output: /ab+c/ object
console.log(deepClone.pattern, typeof deepClone.pattern); // Output: /ab+c/ object

Lodash's cloneDeep method is versatile and can handle complex objects with circular references, making it a reliable choice for deep cloning.



Implications

  • Pros: Robust, well-tested, handles various edge cases.
  • Cons: External dependency, larger bundle size.
  • Performance: Generally efficient, but performance can vary based on object complexity.



2. rfdc (Really Fast Deep Clone): A minimalistic and fast deep cloning library.

const rfdc = require('rfdc')();

const original = {
  name: "Yohan",
  age: 1,
  address: {
    city: "Douala",
    zip: "12345"
  },
  hobbies: [ 'Crying', 'Playing' ],
  birthDate: new Date("2024-02-09"),
  pattern: /ab+c/,
  greet: () => 'Dada'
};

// Deep clone using Lodash's cloneDeep
const deepClone = rfdc(original);

// Modify the nested object and array in the clone
deepClone.address.city = "Alberta";
deepClone.hobbies.push("Eating");

console.log(original.address.city); // Output: Douala
console.log(deepClone.address.city); // Output: Alberta

console.log(original.hobbies); // Output: [ 'Crying', 'Playing' ]
console.log(deepClone.hobbies); // Output: [ 'Crying', 'Playing', 'Eating' ]

console.log(original.birthDate, typeof original.birthDate); // Output: 2024-02-09T00:00:00.000Z object
console.log(deepClone.birthDate, typeof deepClone.birthDate); // Output: 2024-02-09T00:00:00.000Z object

console.log(original.greet, typeof original.greet); // Output: [Function: greet] function
console.log(deepClone.greet, typeof deepClone.greet); // Output: [Function: greet] function



3. deepcopy: A straightforward library that handles deep cloning with ease.

// some code goes here..
const deepClone = deepcopy(original);
// rest of code goes here ...

Install `deepclone` and substitute the line const deepClone = rfdc(original); with const deepClone = deepcopy(original);
You should get the same result.

These third-party libraries provide simple, staright-forward, efficient and reliable deep cloning solutions, making them excellent choices for handling complex objects in JavaScript.


1. Circular References

Circular references occur when an object references itself directly or indirectly, creating a loop.

This can cause problems for many deep cloning methods, such as `JSON` methods, which will throw an error when encountering circular references.

Example of circular reference

const obj = {};
obj.self = obj; // Circular reference

console.log(JSON.parse(JSON.stringify(obj))) // TypeError: Converting circular structure to JSON


This can easily be resolved by using libraries like Lodash’s cloneDeep or rfdc which handle circular references out-of-the-box.

const _ = require('lodash');

const obj = {name: 'Yohan'};
obj.self = obj; // Circular reference

const clone = _.cloneDeep(obj);
console.log(clone); // output : <ref *1> { name: 'Yohan', self: [Circular *1] }


2. Cloning Complex Objects (Functions, DOM Elements, etc.)

Complex objects such as functions, DOM elements, and instances of custom classes often require special handling because standard deep cloning methods do not support them out-of-the-box.

  • Cloning Functions: Functions cannot be deep-cloned using JSON methods or even many deep clone libraries. They need to be copied explicitly.
const originalObj = {
  greet: function() { 
    return 'Hello! I\'m Yohan'; 
  }
};

// Custom function cloning
function cloneFunction(func) {
  const funcStr = func.toString();
  return new Function(`return (${funcStr})`)();
}

const clonedObj = {};
clonedObj.greet = cloneFunction(originalObj.greet);

console.log(clonedObj.greet()); // Outputs: "Hello! I'm Yohan!"


  • Cloning DOM Elements: DOM elements can be cloned using the cloneNode method.
// Assuming we have an html file containing an element with id myElement
const originalElement = document.getElementById('myElement');
const clonedElement = originalElement.cloneNode(true); // true for deep clone
document.body.appendChild(clonedElement);


  • Cloning Instances of Custom Classes: Instances of custom classes require recreating the instance with the same properties
class Person {
  constructor(name, age) {
      this.name = name;
      this.age = age;
  }

  greet() {
      return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
  }
}

// Custom class instance cloning
function cloneInstance(instance) {
  const clone = Object.create(Object.getPrototypeOf(instance));
  Object.assign(clone, instance);
  return clone;
}

const originalPerson = new Person('Yohan', 1);

const clonedPerson = cloneInstance(originalPerson);
console.log(clonedPerson.greet()); // Outputs: "Hello, my name is Yohan and I am 1 years old."


Conclusion

Deep cloning objects in JavaScript is a crucial operation for creating independent copies of complex data structures.

In this guide, we explored various methods and considerations for deep cloning, including:

  • Understanding the difference between shallow and deep cloning.
  • Exploring different methods for deep cloning, such as JSON methods, recursive functions, structured cloning, and third-party libraries like Lodash.
  • Considering performance implications, memory management, and best practices for choosing the right method.
  • Addressing common pitfalls like circular references and cloning complex objects such as functions, DOM elements, and custom classes.


Recommendations
  • Choose the Right Method: Consider the complexity of your objects, performance requirements, and environment constraints when selecting a deep cloning method.
  • Handle Circular References: Be aware of circular references in your objects and use methods or libraries that handle them gracefully.
  • Test Thoroughly: Test your deep cloning implementations thoroughly, especially with complex and nested objects, to ensure correctness and performance.

Deep cloning is a fundamental concept in JavaScript development, and mastering it allows developers to work with complex data structures more effectively.

By understanding the methods, considerations, and best practices outlined in this article, you can confidently tackle deep cloning challenges in their projects.
Happy coding💻🤓!

Interested in exploring more 🤓? Check out these Related Posts.