Deep Copying Objects with the StructuredClone API

Deep Copying Objects with the StructuredClone API


In Javascript, when an object is stored in a variable, the variable is said to contain the reference value of the object, meaning that the variable does not store an object in itself, but instead, an identifier that represents the memory location of the object. Unlike primitives, copying objects works differently.

Shallow Copy vs. Deep Copy

Values can be copied in Javascript in two ways: shallow copy and deep copy.

Shallow Copy

As per MDN

A shallow copy of an object is a copy whose properties share the same references (point to the same underlying values) as those of the source object from which the copy was made. As a result, when you change either the source or the copy, you may cause the other object to change too — and so, you may end up unintentionally causing changes to the source or copy you don’t expect.

This simply means when using shallow copy, only the first level of the object is copied; deeper levels will be referenced. If the original changes, the copy also changes.

In Javascript, a shallow copy can be created using the Object.assign() method:

const theOriginal = {
  someProp: "with a string value",
  anotherProp: {
    withAnotherProp: 1,
    andAnotherProp: true,
  },
};

const theShallowCopy = Object.assign({}, theOriginal);

If we add or change a first-level property on the shallow copy( theShallowCopy). It will not affect the original copy( theOriginal); only the shallow copy where we make the change is affected.

theShallowCopy.aNewProp = "a new value";
console.log(theOriginal.aNewProp)
//  logs `undefined`

However, when we modify a deeply nested property, both the original and shallow copy get affected.

theShallowCopy.anotherProp.aNewProp = "a new value";
console.log(theOriginal.anotherProp.aNewProp) 
//  logs `a new value`

This is because the deeply nested property is referenced, not copied.

Deep Copy

As per MDN

A deep copy of an object is a copy whose properties do not share the same references (point to the same underlying values) as those of the source object from which the copy was made. As a result, when you change either the source or the copy, you can be assured you’re not causing the other object to change too; you won’t unintentionally be causing changes to the source or copy you don’t expect.

This simply means a deep copy creates a new object with its own set of data separate from the original object. If the original object is changed, the copy will not be affected.

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

let theOriginalObject = {
  name: "Mary",
  age: 20,
  address: {
    street: "12 Wall St",
    city: "NY",
    state: "New York",
  },
};

let theDeepCopy = JSON.parse(JSON.stringify(theOriginalObject));

If we modify the address property of the theOriginalObject, the address property of the theDeepCopy will not be modified because they are two different objects:

theOriginalObject.address.state = "california";
console.log(theDeepCopy.address.state); //Output: "New York"

However, It is important to know that deep-copy can also be created using a third-party library like Lodash.

Deep Copying Natively with StructuredClone()

The structuredClone() is a built-in function for deep-copying Javascript values. It uses the structured clone algorithm, which until recently wasn't available to developers and had to be used with workarounds. However, recently updated HTML spec solves this problem by exposing a function called structuredClone() that runs the structured clone algorithm, making it easier to deep copy values in Javascript.

Let’s create a deep copy of an object using structuredClone().

const original = {
  site: "https://blog.openreplay.com/",
  published: new Date(),
  socials: [
    {
      name: "twitter",
      url: "https://twitter.com/openreplay",
    },
    {
      name: "youtube",
      url: "shorturl.at/insT6l", //Subscribe!
    },
  ],
};

const copy = structuredClone(original);

This is the entire API. This is simply how to create a full/deep copy of an object using the structuredClone() function.

Session Replay for Developers

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay— an open-source session replay suite for developers. It can be self-hosted in minutes, giving you complete control over your customer data.

Happy debugging! Try using OpenReplay today.

Why Use structuredClone()?

Since other options are efficiently performant when it comes to deep-copying values in Javascript, why is it important to use the structuredClone() function.

The structuredClone() is not only performant but is also supported in all major browsers.

From canIUSE

structuredClone() vs JSON.parse(JSON.stringify(x))

The JSON.parse(JSON.stringify(x)) JSON-based hack was a commonly used solution for deep-copying before structuredClone(). It was even optimized by V8 due to its popularity. However, It has some shortcomings that structuredClone() addresses.

For Example:

const Person = {
  name: "John",
  date: new Date("2022-03-25"),
  friends: ["Steve", "Karen"]
}

// JSON.stringify converted the `date` to a string
const buggyCopy = JSON.parse(JSON.stringify(Person))

If we log buggyCopy, we will get the following:

{
    name: "John",
    date: "2022-03-25T00:00:00.000Z",
    friends: Array(2)
}

The result is not what is expected, date is supposed to be a Date object, not a string.

JSON is a format that encodes objects in a string. It uses serialization to convert an object into that string and deserialization to its inverse operation (convert string -> object).

This is why JSON.stringify can only handle basic objects, arrays, and primitives. It can be hard to predict how it will work with other types. For example, Date is converted to string and Set to {}.

However, this is not the case with structuredClone():

const Person = {
  name: "John",
  date: new Date("2022-03-25"),
  friends: ["Steve", "Karen"]
}

const bugfreeCopy = structuredClone(Person)

If we log bugfreeCopy, we will get the following:

{
    name: "John",
    date: Object,
    friends: Array(2)
}

When we use structuredClone(), everything works as expected.

structuredClone() vs _.cloneDeep

Lodash’s cloneDeep function is another popular solution used for deep-copying values in Javascript. It’s efficient and works as expected-Check out this codepen.

However, if the function is imported, it will cost you 5.3K gzipped, or if you add the entire library, it will be 25k gzipped. That’s a lot for just creating deep copies. If we also consider the performance cost of using third-party libraries like Lodash’s cloneDeep() function to solve a problem, there is already a native solution for it, and it is better and more performant using the latter.

Limitations of structuredClone()

Though structuredClone() addressed most(not all) of the weak points of the JSON.stringify() method. It has some limitations that are worth paying attention to.

  • Functions can not be copied: If you want to copy an object that contains a function, the DataCloneError exception will be thrown.
//  Error!
structuredClone({ fn: () => { } })
  • DOM nodes can’t be copied: It also throws the DataCloneError when you attempt to clone DOM nodes.
//  Error!
structuredClone({ element: document.body })

Property descriptors, setters, and getters can’t be copied.

  • Prototypes can’t be cloned: Structured cloning doesn’t duplicate the prototype chain. If you copy an instance of a Class, the copied object will no longer be an instance of this Class. A plain object is returned in place of the original Class.
class mainClass { 
  greet = 'hello' 
  Method() { /* ... */ }
}
const instanceClass = new mainClass()

const copied = structuredClone(instanceClass)
// Becomes: { greet: 'hello' }

copied instanceof instanceClass // false

Check out the complete list of supported types that can be copied on MDN; anything not on this list can’t be copied.

Conclusion

Thanks to the arrival of structuredClone, we can now deep-copy values in Javascript easily and natively without the use of workarounds. It is best practice to use solutions already available natively to solve problems; in doing so, we embrace a better JS ecosystem.

Additional Resources


Originally published at dev.to on April 23, 2023.