Deep Cloning Objects in JavaScript, the Modern Way
Tags: javascript, webdev
Published: 2023-01-18 15:03:53

Did you know, there's now a native way in JavaScript to do deep copies of objects?

That's right, this `structuredClone` function is built into the JavaScript runtime:

```jsx
const calendarEvent = {
  title: "Builder.io Conf",
  date: new Date(123),
  attendees: ["Steve"]
}

// 😍
const copied = structuredClone(calendarEvent)
```

Did you notice in the example above we not only copied the object, but also the nested array, and even the Date object?

And all works precisely as expected:

```jsx
copied.attendees // ["Steve"]
copied.date // Date: Wed Dec 31 1969 16:00:00
cocalendarEvent.attendees === copied.attendees // false
```

That’s right, `structuredClone` can not only do the above, but additionally:

- Clone infinitely nested objects and arrays
- Clone circular references
- Clone a wide variety of JavaScript types, such as `Date`, `Set`, `Map`, `Error`, `RegExp`, `ArrayBuffer`, `Blob`, `File`, `ImageData`, and [many more](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#supported_types)
- Transfer any [transferable objects](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects)

So for example, this madness would even work as expected:

```jsx
const kitchenSink = {
  set: new Set([1, 3, 3]),
  map: new Map([[1, 2]]),
  regex: /foo/,
  deep: { array: [ new File(someBlobData, 'file.txt') ] },
  error: new Error('Hello!')
}
kitchenSink.circular = kitchenSink

// ✅ All good, fully and deeply copied!
const clonedSink = structuredClone(kitchenSink)
```

## Why not just object spread?

It is important to note we are talking about a *deep* copy. If you just need to do a shallow copy, aka a copy that does not copy nested objects or arrays, then we can just do an object spread:

```jsx
const simpleEvent = {
  title: "Builder.io Conf",
}
// ✅ no problem, there are no nested objects or arrays
const shallowCopy = {...calendarEvent}
```

Or even one of these, if you prefer

```jsx
const shallowCopy = Object.assign({}, simpleEvent)
const shallowCopy = Object.create(simpleEvent)
```

But as soon as we have nested items, we run into trouble:

```jsx
const calendarEvent = {
  title: "Builder.io Conf",
  date: new Date(123),
  attendees: ["Steve"]
}

const shallowCopy = {...calendarEvent}

// 🚩 oops - we just added "Bob" to both the copy *and* the original event
shallowCopy.attendees.push("Bob")

// 🚩 oops - we just updated the date for the copy *and* original event
shallowCopy.date.setTime(456)
```

As you can see, we did not make a *full* copy of this object. 

The nested date and array are still a shared reference between both, which can cause us major issues if we want to edit those thinking we are only updating the copied calendar event object.

## Why not `JSON.parse(JSON.stringify(x))` ?

Ah yes, this trick. It is actually a great one, and is surprisingly performant, but has some shortcomings that `structuredClone` addresses.

Take this as an example:

```jsx
const calendarEvent = {
  title: "Builder.io Conf",
  date: new Date(123),
  attendees: ["Steve"]
}

// 🚩 JSON.stringify converted the `date` to a string
const problematicCopy = JSON.parse(JSON.stringify(calendarEvent))
```

If we log `problematicCopy`, we would get:

```jsx
{
  title: "Builder.io Conf",
  date: "1970-01-01T00:00:00.123Z"
  attendees: ["Steve"]
}
```

That’s not what we wanted! `date` is supposed to be a `Date` object, not a string.

This happened because `JSON.stringify` can only handle basic objects, arrays, and primitives. Any other type can be handled in hard to predict ways. For instance, Dates are converted to a string. But a `Set` is simply converted to `{}`. 

`JSON.stringify` even completely ignores certain things, like `undefined` or functions.

For instance, if we copied our `kitchenSink` example with this method:

```jsx
const kitchenSink = {
  set: new Set([1, 3, 3]),
  map: new Map([[1, 2]]),
  regex: /foo/,
  deep: { array: [ new File(someBlobData, 'file.txt') ] },
  error: new Error('Hello!')
}

const veryProblematicCopy = JSON.parse(JSON.stringify(kitchenSink))
```

We would get:

```jsx
{
  "set": {},
  "map": {},
  "regex": {},
  "deep": {
    "array": [
      {}
    ]
  },
  "error": {},
}
```

Ew!

Oh yeah, and we had to remove the circular reference we originally had for this, as `JSON.stringify` simply throws errors if it encounters one of those.

So while this method can be great if our requirements fit what it can do, there is a lot that we can do with `structuredClone` (aka everything above that we failed to do here) that this method cannot.

## Why not `_.cloneDeep`?

To date, Lodash’s `cloneDeep` function has been a very common solution to this problem.

And this does, in fact, work as expected:

```jsx
import cloneDeep from 'lodash/cloneDeep'

const calendarEvent = {
  title: "Builder.io Conf",
  date: new Date(123),
  attendees: ["Steve"]
}

// ✅ All good!
const clonedEvent = structuredClone(calendarEvent)
```

But, there is just one caveat here. According to the [Import Cost](https://marketplace.visualstudio.com/items?itemName=wix.vscode-import-cost) extension in my IDE, that prints the kb cost of anything I import, this one function comes in at a whole 17.4kb minified (5.3kb gzipped):

![Screenshot of the import cost of 'lodash/cloneDeep' at 5.3kb gzipped](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8yvegmjha5lc8xu98r6s.png)

And that assumes you import just that function. If you instead import the more common way, not realizing that tree shaking doesn’t always work the way you hoped, you could accidentally import up to [25kb](https://bundlephobia.com/package/lodash@4.17.21) just for this one function 😱

![Screenshot of the import cost of 'lodash' at 25kb](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xmiyi56p07pinttdllka.png)

While that will not be the end of the world to anyone, it’s simply not necessary in our case, not when browsers already have `structuredClone` built in.

## What can `structuredClone` ***not*** clone

### Functions cannot be cloned

They will a throw a `DataCloneError` exception:

```tsx
// 🚩 Error!
structuredClone({ fn: () => { } })
```

### DOM nodes

Also throws a `DataCloneError` exception:

```tsx
// 🚩 Error!
structuredClone({ el: document.body })
```

### Property descriptors, setters, getters

And similar metadata-like features are not cloned. 

For instance, with a getter, the resulting value is cloned, but not the getter function itself (or any other property metadata):

```tsx
structuredClone({ get foo() { return 'bar' } })
// Becomes: { foo: 'bar' }
```

### Object prototypes

The prototype chain is not walked or duplicated. So if you clone an instance of `MyClass`, the cloned object will no longer be known to be an instance of this class (but all valid properties of this class will be cloned)

```tsx
class MyClass { 
  foo = 'bar' 
  myMethod() { /* ... */ }
}
const myClass = new MyClass()

const cloned = structuredClone(myClass)
// Becomes: { foo: 'bar' }

cloned instanceof myClass // false
```

### Full list of supported types

More simply put, anything ***not*** in the below list cannot be cloned:

- [JS Built-ins](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#javascript_types)
    - `Array`, `ArrayBuffer`, `Boolean`, `DataView`, `Date`, `Error` types (those specifically listed below), `Map` , `Object` but only plain objects (e.g. from object literals), [Primitive types](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#primitive_values), *except* `symbol` (aka `number`, `string`, `null`, `undefined`, `boolean`, `BigInt`), `RegExp`, `Set`, `TypedArray`
- Error types
    - `Error`, `EvalError`, `RangeError`, `ReferenceError` , `SyntaxError`, `TypeError`, `URIError`
- [Web/API types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm#webapi_types)
    - `AudioData`, `Blob`, `CryptoKey`, `DOMException`, `DOMMatrix`, `DOMMatrixReadOnly`, `DOMPoint`, `DomQuad`, `DomRect`, `File`, `FileList`, `FileSystemDirectoryHandle`, `FileSystemFileHandle`, `FileSystemHandle`, `ImageBitmap`, `ImageData`, `RTCCertificate`, `VideoFrame`

## Browser and runtime support

And here is the best part - `cloneDeep` is supported in all major browsers, and even Node.js and Deno.

Just note the caveat with Web Workers having more limited support:

![Screenshot of the browser support table linked to below](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ejpjsfbfbf6ytqsc0w4a.png)

Source: [MDN](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone)


## Conclusion

It’s been a long time coming, but we finally now have `structuredClone` to make deep cloning objects in JavaScript a breeze.  Thank you, [Surma](https://web.dev/structured-clone/).

## About me

Hi! I'm [Steve](https://twitter.com/Steve8708?ref_src=twsrc%5Egoogle%7Ctwcamp%5Eserp%7Ctwgr%5Eauthor), CEO of [Builder.io](https://www.builder.io/).

We make a way to drag + drop with your components to create pages and other CMS content on your site or app, visually.

You can read more about how this can improve your workflow [here](https://www.builder.io/blog/headless-cms-workflow).

You may find it interesting or useful:

![Builder.io demo gif](https://res.cloudinary.com/practicaldev/image/fetch/s--Y1qvC5Xj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/au5t8ti43cci5n6t6cfb.gif)