JavaScript structuredClone Table
| Piece | What it does | Field note |
|---|---|---|
structuredClone(obj) | Deep copy, platform-grade | Circular refs and Maps/Sets survive - JSON loses them all |
clone vs JSON roundtrip | Fidelity ladder | JSON: strings/numbers only; clone: Dates, Maps, typed arrays, cycles |
Functions & DOM dropped | Still not magic | Like JSON, uncloneable values throw (not silently drop - LOUDER) |
{ transfer: [buffer] } | Move, don't copy | ArrayBuffers transfer within the same thread - zero-copy deep copy |
vs spread / Object.assign | Shallow imposters | One level deep only - nested objects still share references |
vs library deepClone | The platform won | Lodash deepClone predates it; structuredClone is spec-fast, native |
postMessage uses it | The same engine | Worker and window messaging clone with these exact rules |
prototype chain lost | Class instances → plain | Cloned objects are plain - methods gone, fields copied |
structuredClone is the platform's deep copy: it handles everything JSON roundtripping loses - circular references, Map, Set, Date, RegExp, typed arrays - and it THROWS on the truly uncloneable (functions, DOM nodes) instead of silently dropping them the way JSON.stringify does.
Bottom line: it beats the spread operators (one level deep - nested objects still alias the original) and the deepClone libraries (spec-defined, natively fast), and the transfer option moves big ArrayBuffers zero-copy even within a single thread.
The honest part: clones are PLAIN objects - the prototype chain is lost, so class instances come back as field-correct but method-less shells. State survives; behavior does not. Re-hydrate with Object.assign(new MyClass(), clone) when methods matter.
How to use
- Replace the JSON roundtrip: const copy = structuredClone(state) instead of JSON.parse(JSON.stringify(state)) - same one-liner shape, no Date corruption, no crash on cycles.
- Transfer big buffers: structuredClone(buf, { transfer: [buf] }) - zero-copy move within the same thread; the source reference is neutered, as with workers.
- Snap UI state safely: clone form or undo-stack state on every mutation - clones are plain data, ideal for comparison and rollback.
Frequently asked questions
What can structuredClone copy that JSON.parse(JSON.stringify()) cannot?
The whole modern-data list: circular references (JSON throws), Map and Set (JSON turns them into empty objects), Date (JSON makes ISO strings, not Dates), RegExp, typed arrays, ArrayBuffer, Blob. It also preserves key types more faithfully and refuses gracefully - throwing a DataCloneError on functions and DOM nodes rather than JSON's silent dropping of undefined and functions mid-object (which corrupts shapes quietly). The fidelity ladder: spread/assign at the bottom (one level), JSON roundtrip in the middle (lossy, string-keyed only), structuredClone at the top (spec-complete). One line each - there is no reason to reach below the top rung in modern code.
Why does the prototype chain get lost, and how do I get it back?
structuredClone produces DATA, not objects-with-behavior: the clone is a plain object with the fields copied. A class instance comes back method-less - calling clone.method() throws. The rehydration is explicit: Object.assign(Object.create(MyClass.prototype), clone) or Object.assign(new MyClass(), clone) restores the prototype with the cloned fields. This is also why cloning rarely crosses class boundaries well - it is designed for STATE (the JSON-shaped data layer), and architectures that keep state as plain data (Redux-style, serialized form models) get full value from it, while behavior-heavy class graphs need custom clone methods anyway.
How does the transfer option differ from plain copying?
Ownership move instead of byte copy. { transfer: [arrayBuffer] } hands the buffer's bytes to the clone without duplicating them - instant for any size, but the ORIGINAL's reference is neutered (byteLength 0) exactly like worker postMessage transfers. Use it when the source is done with the data: moving a large buffer from a fetch handler into your state, or detaching scratch memory. The trap is symmetric with workers: after transferring, the source is dead - reading it throws. For small objects the copy is cheaper than thinking; transfer is exclusively the big-buffer optimization.
When is a deep copy actually the wrong tool?
When identity IS the data. Cloning an object graph breaks shared references: two properties pointing at the same object become two independent copies - undo systems, caches and dependency graphs can subtly break when aliasing is meaningful. Deep copies also hide mutation problems instead of fixing them: if state changes are racing, the robust fix is immutability (build new objects per change) or explicit synchronization, not a defensive clone at every read. And cloning at high frequency (every render, every keystroke) has real cost even natively. The honest rule: clone at boundaries (undo snapshots, worker sends, state saves), share references inside.