DEV Community

Cover image for Deep Cloning in JavaScript: The Modern Way. Use structuredClone
Matías Hernández Arellano
Matías Hernández Arellano

Posted on • Originally published at matiashernandez.dev

Deep Cloning in JavaScript: The Modern Way. Use structuredClone

A few days ago, I learned that Javascript has a native way of creating deep copies of an object.

This article will explore the native method for deep cloning an object in JavaScript. We will also discuss the difference between shallow and deep copying in JavaScript.

What is a deep copy or clone?

Javascript objects are usually stored in memory and can only be copied by reference, meaning, that a variable does not store an object in itself, but rather an identifier that represents the memory location of the object. Therefore, objects cannot be copied in the same way as primitives.

There are two types of copy within the Javascript world: shallow and deep.

Shallow Copies

A shallow copy is a copy of an object that only copies the reference to the object, not the actual data. If the original object is modified, the copy will also be modified.

For example, let's say we have an object called "originalObject" that looks like this:


let originalObject = {
  name: "John",
  age: 30,
  address: {
    street: "123 Main St",
    city: "Anytown",
    state: "Anystate"
  }
}
Enter fullscreen mode Exit fullscreen mode

If we create a shallow copy of this object using the Object.assign() method, like this:


let shallowCopy = Object.assign({}, originalObject);
Enter fullscreen mode Exit fullscreen mode

The shallowCopy object will look exactly the same as the originalObject. However, if we modify the address property of the originalObject, the address property of the shallowCopy will also be modified because both objects are pointing to the same address object:


originalObject.address.city = "NewCity";
console.log(shallowCopy.address.city); //Output: "NewCity"
Enter fullscreen mode Exit fullscreen mode

Deep copies

On the other hand, A deep copy creates a new object with its own set of data, separate from the original object. If the original object is modified, the copy will not be affected.

For example, let's say we have an object called "originalObject" that looks like this:


let originalObject = {
  name: "John",
  age: 30,
  address: {
    street: "123 Main St",
    city: "Anytown",
    state: "Anystate"
  }
}
Enter fullscreen mode Exit fullscreen mode

To create a deep copy of this object, we can use the JSON.parse(JSON.stringify(obj)) method.


let deepCopy = JSON.parse(JSON.stringify(originalObject));
Enter fullscreen mode Exit fullscreen mode

If we now modify the address property of the originalObject, the address property of the deepCopy will not be modified because they are completely different objects:


originalObject.address.city = "NewCity";
console.log(deepCopy.address.city); //Output: "Anytown"
Enter fullscreen mode Exit fullscreen mode

Please note that JSON.parse(JSON.stringify(obj)) method is not suitable for complex objects with circular references. In this case, you can use a library such as lodash, underscore or a specific deep copy function.

Native deep cloning: structuredClone

As shown in the previous example, Javascript has ways to work around the deep copy problem. In the example, it uses the serialization strategy. Basically, it transforms an object into a JSON representation and then parses it again.

But, now the Web API has a new way of solving the deep cloning issue by using the structuredClone global method.

This method was recently added to expose the structured clone algorithm, a way to create deep copies of Javascript values that can be used, for example, to transfer JS values from or to a WebWorker.

This algorithm has been part of the HTML spec for a long time but only as a tool used by other APIs. Before structuredClone was added, you have to do some workarounds to use it, like using postMessage to send messages to “ourselves”.

Structured Cloning is an algorithm created and used to transfer values from one real into another, like the postMessage call that send a message to another window or Webworker.

This is a global method that handles the creation of deep copies of any given value.


const deepCopy = structuredClone(originalObject)
Enter fullscreen mode Exit fullscreen mode

Let’s review a quick example.


const original = {
    site: "https://matiashernandez.dev",
    published: new Date(),
    sociales: [{
        name: "twitter",
        url: "https://twitter.com/matiasfha"
    },{
        name: "youtube",
        url: "https://dub.sh/channel" //Subscribe!
    }]
}

const copy = structuredClone(original)
Enter fullscreen mode Exit fullscreen mode

That is all it takes to create a full/deep copy of the original object.

Limitations

Even tho the structured cloning algorithm address many of the issues with JSON.stringify it still has some limitations that you need to be aware of.

  • Can’t clone Function objects: If the object you want to clone contains functions, they will be discarded and a DataCloneError will be thrown.

  • Can’t clone DOM nodes

  • Can’t clone some properties like:

    • lastIndex from a Regexp object
    • setters, getters, and similar metadata
    • the prototype chain will not be duplicated

Alternatives

structuredClone is relatively, but the need for deep cloning values has been there forever. So let’s check some alternatives to the structuredClone method that the web development community has been using

Object.assign and spread syntax.

Object.assign has been the way to create copies for a long time, but here we are talking about deep copy, but Object.assign can only provide shallow copy, and is not able to copy nested objects or arrays.

The spread syntax was added on ES2018 to the properties of objects to provide a convenient way to perform shallow copies, is the equivalent of Object.assign so you can treat them as equals.


const original = {
    site: "https://matiashernandez.dev",
    published: new Date(),
    socials: [{
        name: "twitter",
        url: "https://twitter.com/matiasfha"
    },{
        name: "youtube",
        url: "https://dub.sh/channel" //Subscribe!
    }]
}

const copy1 = Object.assign({}, original)
const copy2 = { ...original }

// Error, this will update the publishedDate property of the original object
copy1.published.setTime(10) 

// Error, this will add an empty object to the original object
copy2.socials.push({})
Enter fullscreen mode Exit fullscreen mode

JSON.parse and JSON.stringify

This has been the trick to get a copy that includes nested objects and arrays; it has a really good performance but is still doesn’t entirely solve the problem of the deep copy.


const original = {
    site: "https://matiashernandez.dev",
    published: new Date(),
    socials: [{
        name: "twitter",
        url: "https://twitter.com/matiasfha"
    },{
        name: "youtube",
        url: "https://dub.sh/channel" //Subscribe!
    }]
}

// The publishedDate property is now a string
const copy = JSON.parse(JSON.stringify(original))
Enter fullscreen mode Exit fullscreen mode

The strategy here is first to transform the object into a string using JSON.stringify this method will serialize each property of the object recursively so, all the nested properties will also be serialized.

Then it uses JSON.parse to “un-serialize” the serialized object and generate a new object from the source.

The problem with this strategy is the serialization process. Every object in Javascript has a property method named toString that implements a way to transform the object into a string representation of itself. Is this implementation the one used by JSON.stringify to serialize each property, the problem comes with properties like the Date object used in the original object, it’s serialized to a string, and a string cannot be transformed back to Date by JSON.parse.

JSON.stringify can only handle basic objects, arrays, and primitives. Other types can be handled in unpredictable ways. For example, Dates are converted to strings, while Sets are simply converted to {}.

lodash.cloneDeep

This has been the “defacto” way of getting a deep copy, but it means you need to add a dependency just to be able to perform a deep copy.

If you import the function, it will cost you 5.3K gzipped, or if you add the entire library, it will be 25k gzipped. That’s a lot if you only want to be able to create deep clones.

Browser Support

The best part of using structuredClone is not only that it has good performance and achieves the task in a good way, but is supported in all major browsers and engines.

Image description
Check the canIUse site

Conclusion

If you need to create deep clones of any value in Javascript, reach for the structuredClone method and “Use the Platform”. It is time to ditch the old habits of using workarounds and embrace a better JS ecosystem.

Top comments (0)