How to deep clone an object in JavaScript

There is a built-in for this now — structuredClone. The question was asked in 2008 and got the answer it deserved at the time; the interesting part today is what each option quietly does to your data.

Why a copy is not a copy

const shallow = { ...src };
shallow.nested.a[0] = "CHANGED";
after editing the copy, the ORIGINAL says: CHANGED

Spread and Object.assign copy the top level. Nested objects are copied as references, so both “copies” point at the same thing.

structuredClone

const c = structuredClone(src);
original after editing the clone: 1

structuredClone is a global in browsers and in Node. One call, no dependency.

What the JSON round-trip destroys

JSON round-trip:
  date     String     "2020-01-01T00:00:00.000Z"
  map      Object     {}
  set      Object     {}
  regexp   Object     {}
  typed    Object     {"0":1,"1":2,"2":3}
  undef    KEY GONE   undefined
  inf      object     null
  nan      object     null

structuredClone:
  date     Date       Wed Jan 01 2020 00:00:00 GMT+0000
  map      Map        [object Map]
  set      Set        [object Set]
  regexp   RegExp     /ab+c/gi
  bigint   BigInt     10
  typed    Uint8Array 1,2,3
  undef    undefined  undefined
  inf      Number     Infinity
  nan      Number     NaN

JSON.parse(JSON.stringify(x)) is the answer that still gets pasted everywhere, and this is what it does. A Date comes back as a string that no longer has .getTime(). A Map and a Set come back as {} — the contents are simply gone, with no error. NaN and Infinity become null, and a key holding undefined disappears entirely, so "k" in obj changes answer.

None of that throws. Two things do:

BigInt         : TypeError: Do not know how to serialize a BigInt
circular ref   : TypeError: Converting circular structure to JSON

structuredClone handles the circular case and keeps the identity:

structuredClone: ok, and cc.self === cc is true

What structuredClone drops

a function  : DOMException: () => 1 could not be cloned.
a class     : x = 1 , constructor = Object , typeof foo.hello = undefined
a prototype : own = 2 , inherited 'proto' = undefined
a getter    : becomes a plain value: 42

A function throws, loudly, which is fine. The second line is the one to remember: cloning a class instance does not throw. You get a plain object with the fields and no methods, and constructor.name is "Object". The failure surfaces later, somewhere else, as “is not a function”.

Getters are flattened to whatever they returned at clone time, and the prototype chain is not followed.

The “efficient” in the question

JSON round-trip        7.87 ms
structuredClone        9.40 ms

10,000 small plain objects — JSON’s best case, and it wins. That is one shape of data and not a general ranking, but it is enough to retire the idea that structuredClone is chosen for speed. It is chosen because it is correct on data the JSON round-trip mangles.

If the object really is plain JSON-shaped data and you are cloning it in a hot loop, the old trick is still defensible. Everywhere else, the losses above are the reason not to.

Where it is not available

structuredClone needs Node 17 or a 2022-era browser. Below that it is lodash.cloneDeep or the JSON round-trip with its losses accepted on purpose.

About Netcup (advertisement)

The German host Netcup offers, among other things, affordable and powerful web hosting packages, KVM-based root servers and dedicated servers. With our voucher codes you can save even more (6€ off your first order, 30% off all KVM-based root servers, ...).