Join us for our biggest AI launch event on 10/31

Announcing Visual Copilot - Figma to production in half the time

Builder.io logo
Contact Sales
Platform
Developers
Contact Sales

Blog

Home

Resources

Blog

Forum

Github

Login

Signup

×

Visual CMS

Drag-and-drop visual editor and headless CMS for any tech stack

Theme Studio for Shopify

Build and optimize your Shopify-hosted storefront, no coding required

Resources

Blog

Get StartedLogin

‹ Back to blog

Web Development

Deep Cloning Objects in JavaScript, the Modern Way

January 18, 2023

Written By Steve Sewell

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:

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:

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
  • Transfer any transferable objects

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

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:

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

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

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

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.

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:

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:

{
  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:

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:

{
  "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.

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

And this does, in fact, work as expected:

import cloneDeep from 'lodash/cloneDeep'

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

const clonedEvent = cloneDeep(calendarEvent)

But, there is just one caveat here. According to the 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

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 just for this one function 😱

Screenshot of the import cost of 'lodash' at 25.2kb gzipped

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.

They will a throw a DataCloneError exception:

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

Also throws a DataCloneError exception:

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

As well as 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):

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

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)

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

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

cloned instanceof myClass // false

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

Array, ArrayBuffer, Boolean, DataView, Date, Error types (those specifically listed below), Map , Object but only plain objects (e.g. from object literals), Primitive types, except symbol (aka number, string, null, undefined, boolean, BigInt), RegExp, Set, TypedArray

Error, EvalError, RangeError, ReferenceError , SyntaxError, TypeError, URIError

AudioData, Blob, CryptoKey, DOMException, DOMMatrix, DOMMatrixReadOnly, DOMPoint, DomQuad, DomRect, File, FileList, FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemHandle, ImageBitmap, ImageData, RTCCertificate, VideoFrame

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

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

Browser support table - screenshot from the link directly below this image

Source: MDN

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.

Tip: Visit our JavaScript hub to learn more.

Hi! I'm Steve, CEO of 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.

You may find it interesting or useful:

Introducing Visual Copilot: convert Figma designs to high quality code in a single click.

Try Visual Copilot

Share

Twitter
LinkedIn
Facebook
Hand written text that says "A drag and drop headless CMS?"

Coming soon: add interactivity and data to your designs

Reserve Your Spot
Newsletter

Like our content?

Join Our Newsletter

Continue Reading
Visual Editing7 MIN
Visual editing is bridging the gap between developers and designers
October 11, 2024
SEO10 MIN
A helpful approach to navigating the SEO AI shift
October 3, 2024
Personalization12 MIN
High-Performance Personalization For Modern Frontends
September 26, 2024