Today's Question:  What does your personal desk look like?        GIVE A SHOUT

Let's talk about JavaScript deep clone

  sonic0002        2023-02-25 08:57:11       3,305        0    

In JavaScript, deep clone means creating a brand new object that includes all nested objects, with all properties being completely independent copies. This is different from shallow copying, which only copies the first-level properties, with nested objects being referenced rather than copied.

There are multiple ways to perform deep copying in JavaScript, but the best one to use depends on the specific use case.

Can use JSON.parse & JSON.stringify? ❌

JSON.parse(JSON.stringify(obj)) is a depth cloning method that uses JSON serialization to create an object. It works for objects that do not contain any circular references(fails if the object contains circular references).

For example:

const obj = { a: 1, b: { c: 2 } };
const clone = JSON.parse(JSON.stringify(obj));

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

In fact, this method is great and unexpectedly performant, but it has a problem in that it cannot handle certain situations well.

Consider this example:

const calendarEvent = {
  title: "source: https://www.builder.io/blog/structured-clone",
  date: new Date(123),
  attendees: ["Lian"]
}

// JSON.stringify converts date to a string
const copy = JSON.parse(JSON.stringify(calendarEvent))

If we print the copy variable, we will get:

{
  title: "source: https://www.builder.io/blog/structured-clone",
  date: "1970-01-01T00:00:00.123Z"
  attendees: ["Lian"]
}

This is not what we want! The date should be a Date object, not a string.

Can use the spread operator to deep clone an object? ❌

No, the spread operator (...) in JavaScript can only perform a shallow clone of an object. When using the spread operator to clone an object, any nested objects will still be referenced rather than copied, so any changes made to nested objects in the cloned object will also affect the original object.

Here's an example:

const original = {a: 1, b: {c: 2}};
const clone = {...original};

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

clone.b.c = 3;

console.log(original);
// Output: {a: 1, b: {c: 3}}

In this example, even though the cloned object is a new object, any changes made to the cloned b property will also affect the original object's b property.

Can use Object.assign to deep clone an object? ❌

No, the Object.assign() method in JavaScript will only perform a shallow clone of an object. When using Object.assign() to clone an object, any nested objects will still be referenced rather than copied, so any changes made to nested objects in the cloned object will also affect the original object.

Here's an example:

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

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

clone.b.c = 3;

console.log(original);
// Output: {a: 1, b: {c: 3}}

In this example, even though the cloned object is a new object, any changes made to the cloned b property will also affect the original object's b property.

Can use the deep cloning methods of Lodash or underscore  _.cloneDeep()? ❌

The Lodash or underscore _.cloneDeep() methods provide a deep cloning function that can handle circular references and other edge cases.

For example:

const _ = require('lodash');
const obj = { a: 1, b: { c: 2 } };
const clone = _.cloneDeep(obj);

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

So far, the cloneDeep function of Lodash has been a common method to solve this problem.

Indeed, this method does work if the object contains Date object:

import cloneDeep from 'lodash/cloneDeep'
const calendarEvent = {
    title: "source: https://www.builder.io/blog/structured-clone",
    date: new Date(123),
    attendees: ["Lian"]
}
// ✅ All good!
const clonedEvent = cloneDeep(calendarEvent)

However, there is a problem. The size of this particular function is quite large, with a size of 17.4kb after minimization and 5.3kb after compression.

This size estimation is based solely on importing the function separately. If you choose the common import approach without considering tree shaking, it may not always be effective, and importing this function separately alone may result in a data volume of up to 25kb.

Although this data size is not large, it is unnecessary in our case, as modern browsers already have a built-in Structured Clone feature, which will be natively implemented later.

Extended Reading: What is Tree shaking?

"Tree shaking" is a technique used by JavaScript bundlers to optimize the size of code. Its main purpose is to eliminate unused code in an application. Through static code analysis, tree shaking can identify and remove unnecessary code, thereby reducing the size of the generated bundle, improving website performance and loading speed.

However, tree shaking is not always effective, especially in scenarios dealing with dynamic module imports, styles, and templates, etc., it is difficult to know exactly which modules to import at build time. At this point, tree shaking may not be able to recognize unused code, so it cannot optimize the size of the code. Therefore, even if tree shaking may be ineffective, code size still needs to be considered at times.

Extended Reading: Can you deep clone all objects? Are there any other problems?

Although the Lodash and Underscore libraries provide the _.cloneDeep() method for deep cloning, these libraries also have some drawbacks.

Firstly, these libraries are third-party libraries that need to be installed and imported. If the project itself does not use these libraries, importing them only for deep cloning may increase the project's size.

Secondly, although these libraries can solve most deep cloning problems, they still cannot copy some specific objects, such as Blob, File, ImageData, and AudioContext, etc.

Finally, these libraries may cause performance issues when dealing with large, complex objects because they need to recursively traverse the entire object and copy each property and nested object. When processing large objects, this can cause serious performance problems.

Therefore, for cases that require deep cloning of specific types of objects, other more appropriate solutions should be sought.

Structured Clone

Structured clone is a concept in JavaScript that refers to the process of deep cloning an object, including its nested objects, arrays, and other complex data structures. The structured clone algorithm is used by several APIs in JavaScript, such as the postMessage method for communication between window objects (e.g., between pages and iframes), the IndexedDB API for storing and retrieving data in browsers, and the Service Workers API for handling network requests in the background.

The structured clone algorithm recursively copies all properties of an object, including nested objects and arrays, to a new object with the same structure and values. It can handle various data types, including functions, dates, regular expressions, arrays, and objects.

The structured clone algorithm is a powerful tool for deep cloning complex data structures in JavaScript and is often used to perform complex data operations in browsers or other JavaScript environments.

Implementing it ourselves

If implementing structured clone ourselves, can use recursive custom implementation- This method involves defining a function that uses recursion to create a deep clone of an object.

Here is an example code using this method:

const original = { a: 1, b: { c: new Date() } };

const deepClone = (obj) => {
  if (obj === null || typeof obj !== "object") return obj;
  let clone = Array.isArray(obj) ? [] : {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] =
        obj[key] instanceof Date
          ? new Date(obj[key].getTime())
          : deepClone(obj[key]);
    }
  }
  return clone;
};

const clone = deepClone(original);

console.log(clone);
// Output: {a: 1, b: {c: 2022-12-31T08:00:00.000Z}}

clone.b.c.setFullYear(2023);

console.log(original);
// Output: {a: 1, b: {c: 2022-12-31T08:00:00.000Z}}

This method can solve all the problems mentioned above. Here is a breakdown of the code:

First, we define an original object called "original" that contains two properties, "a" and "b", where the value of "b" is a date object.

const original = { a: 1, b: { c: new Date() } };

Next, we define a function called deepClone that is used to make a deep clone of any object. This function first checks if the passed object is null or not an object. If it is, it returns the object as is.

const deepClone = (obj) => {
  if (obj === null || typeof obj !== "object") return obj;

Then, based on the type of the passed object, it creates a clone object. If the object is an array, it creates an empty array. Otherwise, it creates an empty object.

let clone = Array.isArray(obj) ? [] : {};

Next, it iterates through all the properties of the original object. If the property is a property of the original object itself (not a property in the prototype chain), it recursively makes a deep copy of that property and assigns it to the corresponding property of the clone object. If the property is a date object, it is treated specially by creating a new date object and setting its timestamp to the timestamp of the original date object.

for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] =
        obj[key] instanceof Date
          ? new Date(obj[key].getTime())
          : deepClone(obj[key]);
    }
  }

Finally, it returns the clone object.

 return clone;
};

Lastly, we verify that the deepClone function successfully creates a deep copy of the original object

const clone = deepClone(original);

console.log(clone);
// Output: {a: 1, b: {c: 2022-12-31T08:00:00.000Z}}

clone.b.c.setFullYear(2023);

console.log(original);
// Output: {a: 1, b: {c: 2022-12-31T08:00:00.000Z}}

The running result indicates that although the clone.b.c was modified, the original object original was not affected. Its property b.c is still a Date object with the year 2022.

Native structuredClone() Method

structuredClone() is a native method in JavaScript and one of the built-in APIs in browsers that can be used for deep cloning objects. When copying an object using structuredClone(), it handles complex data types and ensures that their reference relationships are properly handled. It is one of the best choices for deep cloning objects because it can simultaneously handle various data types and maintain their reference relationships. In addition, it supports multiple environments, including Web Workers and Service Workers.

The advantage of using structuredClone() for deep cloning is that it is a native method in JavaScript and built into the browser, so no additional dependencies are required. Additionally, it can accurately copy all JavaScript basic types and complex type data, including Date, RegExp, Map, and Set, and can accurately copy various objects and data structures. In scenarios where accurate copying of data is needed, such as cross-document communication, storing and restoring application state, using this method can ensure that the copied objects are identical and free from side effects.

However, the disadvantage of using structuredClone() for deep cloning is that it can only be used in a browser environment and not in a Node.js environment. Additionally, it cannot clone certain objects such as Blob, File, ImageData, and AudioContext. It should also be noted that since this method is based on serialization and deserialization, it may not be suitable for large objects and data structures as it may lead to performance issues.

Therefore, when using the structuredClone() method for deep cloning, it is necessary to balance its advantages and disadvantages based on specific situations and ensure that the usage scenario is appropriate.

Further reading

For Blob, File, ImageData, and AudioContext objects, these objects themselves are not serializable and cannot be deeply cloned using structuredClone(). Therefore, in this case, you need to use other methods to copy these objects.

For Blob and File objects, you can use the FileReader API or Blob API to copy these objects. For ImageData objects, you can use the Canvas API to copy it. For AudioContext objects, you can use other methods in the Web Audio API to copy it.

It should be noted that these methods may be more complex than using structuredClone(), but they can handle object types that structuredClone() cannot handle.

Here is an example of the structuredClone() function:

When passing data between two different windows using postMessage(), the data needs to be serialized and deserialized. Since postMessage() only supports the structured clone algorithm, when passing complex data, the structuredClone() method can be used for deep cloning, as shown below:

// sender
const data = { name: 'John', age: 30, hobbies: ['reading', 'swimming'] };
const transfer = [data.hobbies];
const clonedData = structuredClone(data); // use structuredClone() to deep clone
window.opener.postMessage(clonedData, '*', transfer);

// receiver
window.addEventListener('message', function (e) {
  const data = e.data;
  const hobbies = data.hobbies; // object passed
  console.log(data.name); // John
  console.log(data.age); // 30
  console.log(hobbies); // ['reading', 'swimming']
});

In the example above, we send the data object to the parent window using the postMessage() method, and the hobbies array is passed through the transfer array to avoid additional copying when passing the data. On the receiving end, we use the structuredClone() method to deep clone the passed data object to ensure that its property values can be read correctly after receiving the data.

Below is how this function being supported in modern browsers.

Conclusion

This article mainly introduces the correct method of using deep clone in JavaScript, which is very useful for those who want to use it correctly in JavaScript. Although deep cloning may bring performance issues in some cases, it is necessary for handling complex data structures. We need to carefully evaluate our application scenarios and choose the deep clone method that suits us. The best practice is to evaluate the complexity and size of the data structure before deep copying, and it is recommended to use the structured clone method that I recommend.

Reference: 关于JS的深拷贝,你用对方法了吗,现在我们来深入解读下

JAVASCRIPT  DEEP CLONE 

Share on Facebook  Share on Twitter  Share on Weibo  Share on Reddit 

  RELATED


  0 COMMENT


No comment for this article.