How to Clone Complex Data Structures in JavaScript: The Deep Copy and structuredClone Methods

Have you ever encountered unexpected changes to your JavaScript objects or arrays after copying one object to a new variable? This is because, by default, JavaScript performs shallow copies, which means that only the references to the original object or array are copied, not the actual values themselves. It really creates the equivalent of a pointer to the original object. This can lead to unintended consequences and bugs in your code.

How to Clone Complex Data Structures in JavaScript: The Deep Copy and structuredClone Methods

For example, you change the value of the new object's properties, only to find both the copy and the original have the changed value.

Fortunately, there is a solution: deep copying. Deep copying creates an entirely new copy of the original object or array, including all of its nested properties and values. This ensures that any changes made to the copied object or array will not affect the original.

Deep copying is a crucial concept in JavaScript, particularly when dealing with complex data structures. It allows you to maintain data integrity and avoid unexpected changes to your data. In this article, we will explore the challenges of deep copying in JavaScript, as well as the techniques and best practices for performing deep copies of arrays and objects. We will also discuss the new structuredClone() method and how it can be used to handle complex data structures. By the end of this article, you will have a better understanding of deep copying in JavaScript and be equipped with the tools to ensure your data remains intact.

Common Questions and Issues Developers Have Deep Copying in JavaScript

Deep copying in JavaScript can be a tricky subject, and there are several common questions and issues that developers encounter when trying to make deep copies of their data. One of the most frequent issues is the confusion between deep copying and shallow copying. Shallow copying creates a new object that simply points to the same memory location as the original object. Therefore, any changes made to the new object also affect the original object. This is not what we want when we need to make a deep copy.

Another issue that arises is when trying to deep copy objects that contain functions or other non-serializable data. This can result in unexpected behavior, such as lost data or errors. Additionally, deep copying complex data structures, such as nested arrays and objects, can be especially challenging, especially when there are circular references involved.

It's also important to consider performance when making deep copies, as some techniques can be more computationally expensive than others. Overall, there are several considerations and potential pitfalls to keep in mind when making deep copies in JavaScript.

Understanding Deep Copying in JavaScript

In JavaScript, there are two types of copying: shallow copying and deep copying. Shallow copying creates a new object with a reference to the original object, so any changes made to the new object will affect the original object. Deep copying creates a new object with a new reference, so any changes made to the new object will not affect the original object.

Deep copying is important when working with complex data structures, as it ensures that changes made to one instance of the data structure do not affect other instances. However, deep copying in JavaScript can be challenging due to the language's dynamic nature.

One of the challenges of deep copying in JavaScript is dealing with objects that have circular references. Circular references occur when an object refers to itself, either directly or indirectly. For example:

const obj = {};

obj.prop = obj;

Another challenge is handling objects with non-enumerable properties, such as built-in objects like Date, Map, and Set.

To overcome these challenges, there are several techniques for deep copying arrays and objects in JavaScript, which we will discuss in the following sections.

Explanation of the Difference Between Shallow Copying and Deep Copying

When copying objects and arrays in JavaScript, there are two approaches: shallow copying and deep copying. Shallow copying creates a new object or array and copies only the references to the original object's properties or elements. This means that any changes made to the original object will also be reflected in the copied object.

Here's an example of shallow copying an object using the spread operator:

const originalObj = {a: 1, b: 2};

const copiedObj = {...originalObj};

In this example, copiedObj is a shallow copy of originalObj. If we update a property in originalObj, the same change will be reflected in copiedObj:

originalObj.a = 3;

console.log(copiedObj); // {a: 1, b: 2}

On the other hand, deep copying creates a new object or array and copies all of its properties or elements. This means that any changes made to the original object will not be reflected in the copied object, and vice versa.

Here's an example of deep copying an object using JSON.parse(JSON.stringify()):

const originalObj = {a: 1, b: 2};

const copiedObj = JSON.parse(JSON.stringify(originalObj));

In this example, copiedObj is a deep copy of originalObj. If we update a property in originalObj, copiedObj remains unchanged:

originalObj.a = 3;

console.log(copiedObj); // {a: 1, b: 2}

It's important to note that deep copying can be slower and more memory-intensive than shallow copying, especially for large objects or arrays.

Techniques for Deep Copying Arrays in JavaScript

There are several techniques for deep copying arrays in JavaScript, each with its own advantages and disadvantages. Here are some of the most commonly used techniques:

Slice(): The slice() method creates a new array with the elements of the original array. By default, it creates a shallow copy of the array. However, if you pass 0 as the start index and the length of the array as the end index, it will create a deep copy of the array.

Here's an example:

const originalArray = [1, 2, [3, 4]];

const deepCopyArray = originalArray.slice(0, originalArray.length);

Concat(): The concat() method joins two or more arrays and returns a new array. Like slice(), it creates a shallow copy by default, but you can create a deep copy by concatenating an empty array with the original array.

Here's an example:

const originalArray = [1, 2, [3, 4]];

const deepCopyArray = [].concat(originalArray);

JSON.parse(JSON.stringify()): This method is one of the most commonly used techniques for deep copying arrays. It works by converting the array to a JSON string and then parsing the string back into an array. This method is efficient and works well in most cases. However, it has limitations, such as not being able to copy functions or undefined values.

Here's an example:

const originalArray = [1, 2, [3, 4]];

const deepCopyArray = JSON.parse(JSON.stringify(originalArray));

Overall, the technique you choose will depend on your specific use case and the complexity of the array you are copying.

Techniques for Deep Copying Arrays in JavaScript

There are different techniques for deep copying arrays in JavaScript, each with its own advantages and disadvantages. Here are some of the most commonly used methods:

slice(): The slice() method creates a new array and copies all the elements of the original array into it. It takes two optional arguments, start and end, which specify the start and end indices of the portion of the array to be copied. If no arguments are provided, slice() copies the entire array. The advantage of using slice() is that it's a simple and concise method for creating a deep copy of an array.

Example:

const arr1 = [1, 2, 3, 4, 5];

const arr2 = arr1.slice();

console.log(arr2); // [1, 2, 3, 4, 5]

concat(): The concat() method also creates a new array and copies all the elements of the original array into it. However, unlike slice(), it can also merge multiple arrays into one new array. The advantage of using concat() is that it's more flexible than slice() when it comes to copying and merging arrays.

Example:

const arr1 = [1, 2, 3];

const arr2 = [4, 5, 6];

const arr3 = arr1.concat(arr2);

console.log(arr3); // [1, 2, 3, 4, 5, 6]

JSON.parse(JSON.stringify()): This method involves converting the array to a JSON string using JSON.stringify(), and then parsing the JSON string back into a new array using JSON.parse(). The advantage of this method is that it can also be used to deep copy objects with nested arrays and objects. However, it has some limitations, such as not being able to copy functions or properties with undefined or circular values.

Example:

const arr1 = [1, 2, 3];

const arr2 = JSON.parse(JSON.stringify(arr1));

console.log(arr2); // [1, 2, 3]

Overall, the choice of method depends on the specific use case and the requirements for copying and manipulating arrays.

Explanation of the Difference Between Shallow Copying and Deep Copying Objects

In JavaScript, objects are a fundamental data structure used in many applications. When working with objects, it's important to understand the difference between shallow copying and deep copying and to know the techniques for deep copying an object. Deep copying creates a completely new copy of an object, including all of its properties and values, whereas shallow copying only creates a new reference to the same object.

There are several methods for deep copying objects in JavaScript, including:

The spread operator: The spread operator is a concise way to copy all properties of an object into a new object. It works by creating a new object and then copying all the properties of the original object into the new object using the spread syntax.

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

const copiedObject = { ...originalObject };

Object.assign(): Object.assign() is a method that copies all the enumerable properties from one or more source objects to a target object. It returns the target object.

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

const copiedObject = Object.assign({}, originalObject);

JSON.parse(JSON.stringify()): This method is a common way to deep copy an object in JavaScript. It works by first converting the object to a JSON string using JSON.stringify(), and then parsing the JSON string back into an object using JSON.parse(). This creates a new object that is completely separate from the original.

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

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

The new structuredClone() method: The structuredClone() method is a new addition to JavaScript that provides a standardized way to deep copy complex data structures. It can be used to deep copy objects, arrays, and other complex data structures, and can handle circular references and other challenges of deep copying.

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

const copiedObject = structuredClone(originalObject);

Overall, there are several methods available for deep copying objects in JavaScript. The method you choose will depend on your specific use case and the performance considerations of your application.

Deep Copying Complex Data Structures in JavaScript

When it comes to deep copying complex data structures in JavaScript, things can get a bit trickier. Deep copying an array or object is relatively straightforward, but when you start dealing with nested arrays and objects or objects with circular references, the process becomes more challenging.

To deep copy a nested array, you need to make sure that each level of the array is copied recursively. Here is an example of how to deep copy a nested array using the spread operator:

const arr1 = [1, [2, 3], [4, [5, 6]]];

const arr2 = [...arr1.map(item => Array.isArray(item) ? [...item] : item)];

console.log(arr2); // [1, [2, 3], [4, [5, 6]]]

In this example, we use the map() method to iterate over the original array and create a new array with the spread operator. If an item in the original array is itself an array, we use the spread operator to create a copy of it, and if it is not an array, we just copy the value.

Deep copying an object with nested properties requires a similar recursive approach. One way to do this is to use a combination of the spread operator and Object.entries() to create a copy of each property in the object, including any nested properties. Here is an example:

const obj1 = {

  a: 1,

  b: {

    c: 2,

    d: {

      e: 3

    }

  }

};



const obj2 = Object.fromEntries(

  Object.entries(obj1).map(([key, value]) =>

    [key, typeof value === "object" && value !== null ? {...value} : value]

  )

);



console.log(obj2); // { a: 1, b: { c: 2, d: { e: 3 } } }

In this example, we use the Object.entries() method to get an array of [key, value] pairs for each property in the object. We then use map() to create a new array with the spread operator, copying each property's value recursively. Finally, we use Object.fromEntries() to turn the array of [key, value] pairs back into an object.

Objects with circular references are a bit more challenging to deep copy. One approach is to keep track of the objects that have already been copied and refer to the new copies instead of the original ones. Here's an example:

function deepCopy(obj, copies = new WeakMap()) {

  // Check if we've already copied this object

  if (copies.has(obj)) {

    return copies.get(obj);

  }



  let copy;



  // Handle different types of objects

  if (obj instanceof Array) {

    copy = obj.map(item => deepCopy(item, copies));

  } else if (obj instanceof Object) {

    copy = Object.fromEntries(

      Object.entries(obj).map(([key, value]) =>

        [key, deepCopy(value, copies)]

      )

    );

  } else {

    return obj;

  }



  copies.set(obj, copy);



  return copy;

}



const obj1 = { a: 1 };

const obj2 = { b: { c: 2 } };

obj1.b = obj2;

obj2.a = obj1;



const obj3 = deepCopy(obj1);



console.log(obj3); // { a: 1, b: { c: 2, a: [Circular] } }

In this example, we define a deepCopy() function that takes an object and a copies map.

Overview of How the structuredClone() Method Handles Complex Data Structures

The structuredClone() method is a relatively new addition to JavaScript. It was designed to handle deep copying of complex data structures, including nested arrays and objects, as well as objects with circular references.

The structuredClone() method is similar to the JSON.stringify() and JSON.parse() methods in that it converts JavaScript objects into a string format, but it has a few key differences. First, structuredClone() is able to handle complex data structures, including nested arrays and objects, and objects with circular references, which JSON.stringify() and JSON.parse() cannot handle. Second, structuredClone() creates a new copy of the object, whereas JSON.parse() creates a reference to the original object.

The structuredClone() method is also faster than some of the other deep copying techniques, especially for large and complex data structures. This is because it performs the copying operation at a lower level, which allows it to take advantage of lower-level optimizations.

The structuredClone() method is supported by all major browsers, including Edge, Chrome, Firefox, and Safari. And yes Nodejs also supports this magical new native method.

structuredClose API Support by Browser

Here is an example of using the structuredClone() method to deep copy a complex data structure:

In the example above, the structuredClone() method is used to create a deep copy of the obj object. The copy variable is assigned the new copy of the object, which can be used independently of the original object.

let obj = {

  name: 'John',

  age: 30,

  hobbies: ['reading', 'hiking', 'swimming'],

  address: {

    street: '123 Main St',

    city: 'Anytown',

    state: 'CA'

  }

};



let copy = structuredClone(obj);



console.log(copy);

// Output: {name: "John", age: 30, hobbies: Array(3), address: {…}}

Summary

Deep copying in JavaScript is an important concept that allows developers to create independent copies of data structures. Shallow copying and deep copying have distinct differences and can lead to unexpected results if not implemented correctly.

There are various techniques for deep copying arrays and objects in JavaScript, including slice(), concat(), spread operator, Object.assign(), JSON.parse(JSON.stringify()), and the new structuredClone() method. Each method has its own advantages and disadvantages, and choosing the appropriate one depends on the specific use case.

In addition, deep copying complex data structures such as nested arrays and objects, arrays of objects, and objects with circular references can be challenging, but the structuredClone() method offers a solution that can handle complex data structures more efficiently.

When considering best practices for deep copying in JavaScript, performance considerations should be taken into account. It is important to choose a technique that balances speed and memory usage.

While deep copying can be challenging, there are multiple techniques available in JavaScript to achieve this task. The new structuredClone() method offers a promising solution for deep copying complex data structures. By understanding the differences between shallow and deep copying and selecting the appropriate technique for each situation, developers can ensure their code functions as intended.

Thank you for reading this article on deep copying in JavaScript. I hope you found it informative and helpful in your development work.

If you enjoyed this article, please consider liking and sharing it with your colleagues and peers. You can also follow me as an author here on C# Corner for more great articles on web development, PWAs, and artificial intelligence.

Thank you again for your support, and I look forward to bringing you more valuable content in the future.


Love2Dev
We specialize in Progressive Web Apps and Web Performance Optimization