JavaScript deep copy: structuredClone, JSON, and when each one breaks

javascript deep copy, deep copy javascript, js deep copy, and deep copy object javascript: shallow spread and Object.assign, structuredClone for nested cloneable data, JSON.parse(JSON.stringify) limits, lodash cloneDeep, jQuery.extend in legacy apps, and a small JSON-shaped recursive helper. Runnable snippets tested on Node.js v20.18.2.

Published

Updated

Read time 7 min read

Reviewed byDeepak Prasad

JavaScript deep copy: structuredClone, JSON, and when each one breaks

Queries such as javascript deep copy, deep copy javascript, js deep copy, and deep copy object javascript usually mean either duplicating only the top-level container or duplicating nested objects and arrays so edits on the clone do not leak back to the original.

This guide walks through shallow versus deep behavior, the built-in structuredClone path, JSON and library options, and a small recursive helper for JSON-shaped data. The structuredClone API is available in modern browsers and in Node.js 17+ under the same global name.

Tested on: Node.js v20.18.2. A short note after each runnable snippet describes what you should see in the console.


Quick reference

Goal Typical API
Deep clone cloneable plain data (incl. cycles, Date, Map, Set) structuredClone(value)
Deep clone JSON-only trees JSON.parse(JSON.stringify(value))
Deep clone with Lodash semantics / edge cases cloneDeep from lodash
Copy top level only { ...obj }, Object.assign({}, obj)

Shallow vs deep (why spread is not enough)

  • Shallow copy: only the first level is copied; nested objects and arrays remain shared references.
  • Deep copy: every nested plain value you care about is duplicated, within the limits of the method you use.

Object spread is shallow for nested data:

javascript
const orig = { a: 1, nested: { x: 1 } };
const spr = { ...orig };
spr.nested.x = 99;
console.log(orig.nested.x, spr.nested.x);
Output

You should see one line with two 99 values—mutating the copy’s nested object changed the original too.

So deep copy in javascript for nested graphs is not solved by { ...obj } or Object.assign({}, obj) alone.


1. structuredClone() (default for cloneable data)

The global structuredClone function applies the structured clone algorithm (the same family as postMessage and IndexedDB serialization). For deep copy js scenarios where values are plain data, Dates, collections, and similar cloneable types—and no functions—it is usually the first API to try.

javascript
const original = {
  n: [1, { y: 2 }],
  d: new Date(0),
  m: new Map([['a', 1]]),
};

const copy = structuredClone(original);
copy.n[1].y = 3;

console.log(original.n[1].y, copy.n[1].y);
console.log(copy.d instanceof Date, copy.d.getTime());
console.log(copy.m.get('a'));
Output

You should see three lines: nested y stays 2 on the original and 3 on the copy; copy.d is a real Date at epoch; the cloned Map still maps 'a' to 1.

Circular references, which break JSON.stringify, are preserved:

javascript
const circ = { a: 1 };
circ.self = circ;
const circ2 = structuredClone(circ);
console.log(circ2.self === circ2);
Output

You should see true—the clone’s self points back at the clone, not the original graph.

Typical failure: functions are not cloneable.

javascript
try {
  structuredClone({ fn: () => {} });
} catch (e) {
  console.log(e.name);
}
Output

You should see one line: DataCloneError.

Other limitations are spelled out in MDN’s structured clone algorithm notes: prototype chains and property descriptors are not preserved the way a hand-written copier might. For nested JSON workflows in your stack, see searching nested JSON.

Runtime: structuredClone exists in Node.js 17+ and current browsers; for older targets you may need a polyfill such as @ungap/structured-clone. Verify engine support before relying on it in production.


2. JSON.parse(JSON.stringify(value)) (plain JSON trees only)

Use this when the graph is small, acyclic, and JSON-safe: it is quick to write and needs no dependencies, but undefined keys disappear, Date / Map / Set / BigInt do not round-trip the way you might expect, and circular references throw.

javascript
const originalObject = {
  name: 'Deepak',
  age: 45,
  hobbies: ['blogging', 'reading'],
};

const newObject = JSON.parse(JSON.stringify(originalObject));
newObject.name = 'Dave';
newObject.hobbies.push('drawing');

console.log(JSON.stringify(originalObject));
console.log(JSON.stringify(newObject));
Output

You should see two JSON strings: the first still shows Deepak with two hobbies; the second shows Dave with three hobbies—proving the top-level object was duplicated but the inner array was shared until you mutated through the copy.

undefined is dropped:

javascript
console.log(JSON.stringify(JSON.parse(JSON.stringify({ a: 1, u: undefined }))));
Output

You should see {"a":1}—the undefined value did not survive the round-trip.

Circular structures throw:

javascript
const circ = { a: 1 };
circ.self = circ;
try {
  JSON.parse(JSON.stringify(circ));
} catch (e) {
  console.log(e.name);
}
Output

You should see TypeErrorJSON.stringify cannot serialize a cyclic graph.


3. Spread / Object.assign (shallow — know the limits)

These APIs copy top-level keys into a new object or array while keeping nested references shared. Use them when you intend to share nested state, or before handing values to a deep cloner—but they are not a javascript deep copy object solution on their own.

Top-level arrays of primitives still get a new array (each index holds a primitive value):

javascript
const originalArray = [13, 874, 192];
const newArray = [...originalArray];
console.log(JSON.stringify(newArray));
newArray[1] = 35;
console.log(JSON.stringify(originalArray));
console.log(JSON.stringify(newArray));
Output

You should see three JSON lines: first the cloned array, then the original unchanged after you only replaced index 1 on the copy, then the copy with 35 in the middle slot.

Object spread clones top-level keys only:

javascript
const originalObject = {
  name: 'Femi',
  skill: 'frontend',
  language: 'javascript',
  level: 2,
};

const newObject = { ...originalObject };
console.log(newObject);
Output

You should see one object log with the same four keys and values as the source.

Nested mutation with Object.assign:

javascript
const originalObject = {
  name: 'Deepak',
  age: 45,
  hobbies: ['blogging', 'reading'],
};

const newObject = Object.assign({}, originalObject);
newObject.name = 'Dave';
newObject.hobbies.push('mutated-on-copy');

console.log(JSON.stringify(originalObject));
console.log(JSON.stringify(newObject));
Output

You should see two JSON strings that both list the extra hobby—Object.assign gave a new top-level object but the hobbies array is still one shared instance.

Notice the original hobbies array changed because the copy still points at the same array instance.


4. Lodash cloneDeep (libraries for tricky graphs)

Install Lodash when you need its cloning semantics (including some values structuredClone rejects) or you already depend on the library. See cloneDeep in the Lodash docs for full behavior.

bash
npm install lodash
javascript
const { cloneDeep } = require('lodash');

const originalObject = {
  name: 'Deepak',
  age: 45,
  hobbies: ['blogging', 'reading'],
};

const copyObject = cloneDeep(originalObject);
copyObject.hobbies.push('swimming');

console.log(JSON.stringify(originalObject.hobbies));
console.log(JSON.stringify(copyObject.hobbies));
Output

You should see the original hobbies unchanged in the first line, and the copy’s hobbies including swimming in the second.


5. jQuery $.extend(true, {}, obj) (legacy stacks only)

$.extend(true, target, source) recursively copies into target. It is mainly relevant for older codebases that already load jQuery; new projects should prefer structuredClone or small utilities.

The snippet below wires jsdom + jquery the way you might in a local Node script. It uses {run=false} because the hosted Run control may not install those packages.

javascript
const { JSDOM } = require('jsdom');
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
  url: 'http://localhost',
});
const { window } = dom;
const $ = require('jquery')(window);

const originalObject = {
  name: 'Deepak',
  age: 45,
  hobbies: ['blogging', 'reading'],
};

const copyObject = $.extend(true, {}, originalObject);
copyObject.hobbies.push('swimming');

console.log(JSON.stringify(originalObject.hobbies));
console.log(JSON.stringify(copyObject.hobbies));

You should see the same split as the Lodash example: only the deep-copied object’s array gained swimming.


6. Hand-written recursion (JSON-like objects only)

A tiny recursive function can walk plain objects and arrays without extra dependencies, but naive copy = {} logic breaks real arrays (push is missing). The version below handles arrays with Array.isArray; it still does not replace structuredClone for Map, Set, Date, cycles, or typed arrays.

javascript
function deepCopyJsonLike(value) {
  if (value === null || typeof value !== 'object') return value;
  if (Array.isArray(value)) return value.map(deepCopyJsonLike);
  const out = {};
  for (const key of Object.keys(value)) {
    out[key] = deepCopyJsonLike(value[key]);
  }
  return out;
}

const originalObject = {
  name: 'Deepak',
  age: 45,
  hobbies: ['blogging', 'reading'],
};

const copyObject = deepCopyJsonLike(originalObject);
copyObject.hobbies.push('coding');

console.log(JSON.stringify(originalObject.hobbies));
console.log(JSON.stringify(copyObject.hobbies));
Output

You should see the original hobbies untouched, then the copy with coding appended.


Summary

Start with structuredClone for plain cloneable data; fall back to JSON, Lodash, or hand recursion only when their trade-offs match your graph.

Approach Deep for nested plain data? Cycles Date / Map / Set Functions
{ ...obj } / Object.assign No N/A references shared copied by ref if value
JSON round-trip Yes (JSON-safe only) No poor / lossy dropped
structuredClone Yes (cloneable types) Yes Yes throws
lodash.cloneDeep Yes (Lodash rules) Yes Yes Yes

For most javascript deep copy object work today, start with structuredClone. Use JSON only for strict JSON-shaped trees, and reach for Lodash or custom code when cloneability rules or prototypes require it.


References

For how objects are normally written in JS, see object literals. For shallow array duplication patterns, see copy array and spread operator.


Frequently Asked Questions

1. Is object spread or Object.assign a deep copy in JavaScript?

No for nested data. They copy top-level keys into a new object, but nested objects and arrays stay shared. Mutating a nested array on the copy can change the original.

2. When is JSON.parse(JSON.stringify(obj)) safe for a deep copy?

Only for JSON-serializable trees: no cycles, no undefined values you need to keep, and no Date, Map, Set, or BigInt you expect to survive unchanged. It throws on circular structures and drops undefined keys.

3. What should I use first for deep copy object js in modern runtimes?

Prefer structuredClone when the value only contains structured-cloneable data. It handles nested objects, arrays, Date, Map, Set, and circular references. It throws DataCloneError for functions and some host objects.

4. structuredClone vs lodash cloneDeep — how do I choose?

Start with structuredClone for standard plain data. Use cloneDeep when you rely on Lodash semantics, need to clone values structuredClone rejects, or already depend on Lodash.

5. Does deep copy in js with structuredClone preserve class prototypes?

No. The clone algorithm does not preserve the prototype chain like new MyClass() would. You get plain-object-shaped data unless you implement your own copying strategy.

6. Where does hand-written recursion still fit?

For small JSON-like graphs without Map, Set, Date, cycles, or typed arrays. Naive copy = {} recursion breaks arrays; use Array.isArray branches or use structuredClone instead.

7. What about Immer or other immutability libraries?

They optimize produce and draft workflows rather than a one-off clone of arbitrary graphs. Use them when your app already follows that pattern; use structuredClone or cloneDeep for ad-hoc deep copies.
Olorunfemi Akinlua

Boasting over five years of experience in JavaScript, specializing in technical content writing and UX design. With a keen focus on programming languages, he crafts compelling content and designs …