Back to Blog
Performanceintermediate8 min read

How to Deep Clone an Object in JavaScript (5 Ways)

Learn 5 ways to deep clone an object in JavaScript from structuredClone to lodash. Includes performance benchmarks and gotcha guide.

S
SnipShift Team·

How to Deep Clone an Object in JavaScript (5 Ways)#

If you've ever mutated a nested object and watched a completely different part of your app break, you already know why deep cloning matters. It's one of those things that seems like it should be trivial just copy the object, right? but JavaScript has a habit of making "simple" things surprisingly tricky.

The need to deep clone an object in JavaScript comes up constantly. Reducer logic in Redux, undo/redo systems, caching layers, testing fixtures. And for years, the ecosystem sort of just... hacked around it. We had the JSON trick, lodash, hand-rolled recursive functions. None of them were great.

But things have changed. We finally have a native, proper way to do it. So let me walk you through all five approaches what they're good at, where they fall apart, and which one you should actually use in 2026.

Quick Refresher: Shallow Copy vs Deep Clone#

Before we get into the five methods, let's make sure we're on the same page about why shallow copies aren't enough. If you've worked with JavaScript destructuring, you've probably run into this already.

A shallow copy duplicates the top-level properties of an object. But if any of those properties are objects themselves (arrays, nested objects, dates), the copy just holds a reference to the same thing in memory. Change the nested object in the copy, and you've changed the original too.

javascript
const original = {  name: "Alice",  settings: { theme: "dark", fontSize: 14 }};// Shallow copy  settings is shared!const shallow = { ...original };shallow.settings.theme = "light";console.log(original.settings.theme); // "light"  oops

That's the bug. And it's sneaky, because shallow.name is completely independent it's only the nested stuff that bites you. A deep clone creates a fully independent copy, all the way down. No shared references. No surprises.

Here's how that distinction looks at a high level:

mermaid
graph TD    A[Original Object] --> B{Copy Method}    B -->|Shallow Copy| C[New top-level object]    C --> D[Nested objects still shared ⚠️]    B -->|Deep Clone| E[New top-level object]    E --> F[Nested objects fully independent ]

Alright, let's get into the five ways to actually do this.

Method 1: structuredClone() The Modern Answer#

This is the one you should reach for first. structuredClone() landed in all major browsers and Node.js 17+, and it's been stable long enough that there's really no excuse not to use it. It's the first time JavaScript has had a built-in, proper deep clone and honestly, it's kind of amazing that it took this long.

javascript
const original = {  name: "Config",  created: new Date("2026-01-15"),  items: [1, 2, { nested: true }],  data: new Map([["key", "value"]]),  buffer: new ArrayBuffer(8)};const clone = structuredClone(original);// Fully independent copyclone.items[2].nested = false;console.log(original.items[2].nested); // true  original untouched// Date objects are real Dates, not stringsconsole.log(clone.created instanceof Date); // true// Maps, Sets, ArrayBuffers  all handledconsole.log(clone.data.get("key")); // "value"

The big win here over the JSON trick is that structuredClone handles dates, Maps, Sets, ArrayBuffers, RegExps, and even circular references. It uses the same algorithm the browser uses to pass data between web workers the structured clone algorithm so it's battle-tested infrastructure, not a hack.

What it can't clone:

  • Functions (throws an error)
  • DOM nodes
  • Symbols
  • Property descriptors, getters/setters (lost during cloning)
  • Prototype chain (you get plain objects back)

For 95% of real-world use cases state management, API response caching, test fixtures this is everything you need. I've been using it in production for over a year now, and I honestly can't remember the last time I needed something it didn't support.

Tip: If you're migrating a codebase from lodash's cloneDeep to structuredClone, run your test suite first. The edge cases around functions and prototypes can catch you off guard if your objects are fancier than plain data.

Method 2: JSON.parse(JSON.stringify()) The Classic Hack#

This was the way to deep clone an object in JavaScript for about a decade. And it works sort of. It serializes your object to a JSON string and then parses it back into a fresh object. Quick, clever, and available everywhere.

javascript
const original = {  name: "Settings",  preferences: { notifications: true, volume: 80 }};const clone = JSON.parse(JSON.stringify(original));clone.preferences.volume = 50;console.log(original.preferences.volume); // 80  works fine for plain data

For simple objects with strings, numbers, booleans, arrays, and nested plain objects this is fine. But the moment you step outside of JSON-safe territory, things go sideways fast.

Here's a real gotcha list that I've personally tripped over:

javascript
const problematic = {  date: new Date(),           // becomes a string  regex: /pattern/gi,          // becomes empty object {}  undef: undefined,            // property disappears entirely  fn: () => "hello",           // property disappears  nan: NaN,                    // becomes null  infinity: Infinity,          // becomes null  map: new Map([["a", 1]]),    // becomes empty object {}  set: new Set([1, 2, 3])      // becomes empty object {}};const clone = JSON.parse(JSON.stringify(problematic));console.log(clone.date);       // "2026-03-25T10:30:00.000Z" (string, not Date)console.log(clone.regex);      // {}console.log(clone.undef);      // undefined (property gone)console.log(clone.fn);         // undefined (property gone)console.log(clone.nan);        // null

And if your object has circular references? JSON.stringify throws a TypeError. No fallback, no warning just a crash.

Should you still use this? Honestly, in 2026, no. Not unless you're stuck in an environment without structuredClone (some older embedded JS runtimes, maybe). The gotchas are too numerous and too silent your data just quietly becomes wrong, and you find out three days later when something downstream breaks.

Method 3: Spread Operator / Object.assign() Shallow Only#

I'm including this because I've seen it in pull requests more times than I can count, labeled as a "deep clone." It's not. The spread operator and Object.assign() are shallow copy mechanisms. But since so many people confuse them with deep cloning, it's worth being explicit.

javascript
const original = {  name: "User",  address: { city: "Austin", state: "TX" }};// Both of these are shallow copiesconst spread = { ...original };const assigned = Object.assign({}, original);spread.address.city = "Denver";console.log(original.address.city); // "Denver"  mutation leaked through

The spread operator copies the top-level properties by value (for primitives) or by reference (for objects). It does NOT recurse into nested structures. Same with Object.assign().

When spread actually makes sense:

  • Flat objects with no nesting
  • Creating a new object with some overridden properties: { ...config, debug: true }
  • React state updates where the nesting is only one level deep

But the moment you have nested objects, arrays of objects, or anything more than one level deep spread is not your friend for cloning. Use it for what it's good at (merging, overriding), and use a real deep clone for everything else.

Method 4: Lodash cloneDeep The Workhorse#

For years, _.cloneDeep() was the gold standard. It handles just about everything nested objects, arrays, Dates, RegExps, Maps, Sets, Buffers, circular references, even typed arrays and symbol-keyed properties. The lodash team spent years handling edge cases that nobody else bothered with.

javascript
import cloneDeep from 'lodash/cloneDeep';const original = {  users: [    { name: "Alice", joined: new Date("2024-06-15") },    { name: "Bob", joined: new Date("2025-01-20") }  ],  metadata: new Map([["version", 3]]),  pattern: /^user_\d+$/i};const clone = cloneDeep(original);clone.users[0].name = "Alicia";console.log(original.users[0].name); // "Alice"  untouchedconsole.log(clone.users[0].joined instanceof Date); // true

The trade-off is bundle size. Even if you import just cloneDeep from lodash/cloneDeep (and you should don't import all of lodash for one function), you're still pulling in about 15-18KB minified. That's not nothing, especially if deep cloning is the only thing you need lodash for.

Here's my take: if your project already uses lodash for other things, cloneDeep is great. Keep using it. But if you'd be adding lodash just for cloning? Use structuredClone instead. There's no reason to add a dependency for something the platform does natively now.

If you're working on converting your JavaScript codebase to TypeScript, lodash has solid type definitions and cloneDeep preserves the generic type, so cloneDeep<Config>(obj) gives you back a Config. That said, structuredClone does the same thing now. And if you want to quickly prototype your types, SnipShift's JS to TypeScript converter can generate proper interfaces for the objects you're cloning way faster than writing them by hand.

Method 5: Custom Recursive Function Full Control#

Sometimes you need something structuredClone can't do. Maybe you need to clone objects with class instances and preserve the prototype chain. Maybe you need to transform values during cloning like converting all Date objects to ISO strings, or stripping out certain keys. A custom recursive deep clone gives you that control.

Here's a solid implementation that handles the common cases:

🔧

Try the JS to TypeScript

Paste your code, get the result instantly. AI-powered, free, no signup.

Open JS to TypeScript
javascript
function deepClone(obj, seen = new WeakMap()) {  // Handle primitives and null  if (obj === null || typeof obj !== "object") return obj;  // Handle circular references  if (seen.has(obj)) return seen.get(obj);  // Handle Date  if (obj instanceof Date) return new Date(obj.getTime());  // Handle RegExp  if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);  // Handle Map  if (obj instanceof Map) {    const mapClone = new Map();    seen.set(obj, mapClone);    obj.forEach((val, key) => mapClone.set(deepClone(key, seen), deepClone(val, seen)));    return mapClone;  }  // Handle Set  if (obj instanceof Set) {    const setClone = new Set();    seen.set(obj, setClone);    obj.forEach((val) => setClone.add(deepClone(val, seen)));    return setClone;  }  // Handle Arrays and plain Objects  preserve prototype  const clone = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));  seen.set(obj, clone);  for (const key of Reflect.ownKeys(obj)) {    const descriptor = Object.getOwnPropertyDescriptor(obj, key);    if (descriptor.value !== undefined) {      clone[key] = deepClone(descriptor.value, seen);    } else {      Object.defineProperty(clone, key, descriptor);    }  }  return clone;}

This version handles circular references (via WeakMap), preserves prototypes, clones symbol-keyed properties, and handles Dates, RegExps, Maps, and Sets. You can extend it for your specific needs add a case for your custom class, skip certain keys, transform values on the fly.

The downside? You're maintaining it. Every edge case you didn't think of is a potential bug. I've used custom clone functions in two production apps, and both times I ended up adding edge case handlers for months after the initial implementation. If you go this route, write extensive tests.

Warning: Custom recursive cloning is powerful but fragile. If you're cloning deeply nested objects (100+ levels), you might hit the call stack limit. For most real-world data structures this won't be an issue, but it's worth knowing about.

Performance Comparison#

Here's where it gets interesting. I ran benchmarks on a moderately complex object (3 levels of nesting, a couple arrays, a Date, a Map) 10,000 iterations, Node.js 22, averaged over 5 runs. Your numbers will vary depending on object shape and runtime, but the relative ranking is pretty consistent.

MethodAvg Time (10k ops)Handles DatesHandles Maps/SetsCircular RefsBundle Cost
structuredClone()~45msYesYesYes0 KB (native)
JSON.parse/stringify~30msNo (becomes string)No (becomes {})No (throws)0 KB (native)
Spread / Object.assign~8msN/A (shallow only)N/A (shallow only)N/A0 KB (native)
Lodash cloneDeep~65msYesYesYes~17 KB
Custom recursive~40msDepends on implDepends on implDepends on impl~0.5 KB

A few things jump out:

JSON.parse/stringify is fast faster than structuredClone in raw speed. That surprises people. The V8 team has spent years optimizing JSON parsing, and it shows. But speed doesn't matter if your data comes back wrong. A fast clone that silently drops your dates and maps is worse than a slower clone that gets it right.

Spread is by far the fastest because it's doing the least work. Shallow copies are cheap. But again, it's solving a different problem.

structuredClone and the custom function are neck and neck. For most applications, the difference is negligible. The custom function has an edge if you've tailored it to your exact data shape, but structuredClone wins on zero maintenance.

Lodash is the slowest, which makes sense it's doing the most. It handles more edge cases than any other method, including typed arrays, Buffers, and argument objects. You're paying for completeness.

So Which One Should You Use?#

Here's my honest recommendation, boiled down:

Use structuredClone() for 95% of cases. It's native, handles the important types correctly, manages circular references, and costs you nothing in bundle size. This should be your default.

Use the JSON trick only if you're 100% sure your data is JSON-safe (no Dates, no undefined, no special types) and you're in a performance-critical hot path where those 15ms matter across thousands of operations.

Use spread when you need a shallow copy. Don't pretend it deep clones.

Use lodash cloneDeep if your project already depends on lodash and you're dealing with exotic data types that structuredClone doesn't handle like preserving class prototypes or cloning objects with Symbol keys.

Use a custom function when you need behavior that none of the above provide prototype preservation, selective cloning, value transformation during the clone. And write tests. Lots of tests.

Understanding the difference between JavaScript objects and their serialized forms is sort of fundamental to this whole topic. If you want to go deeper on that relationship, our post on JavaScript objects vs JSON covers the nuances that trip people up especially around the types that get lost in serialization.

One More Thing: TypeScript and Cloned Objects#

If you're writing TypeScript and you probably should be in 2026 all five methods preserve the type of your cloned object. structuredClone is generic: structuredClone<MyType>(obj) returns MyType. Same with lodash's cloneDeep. The JSON trick works too since TypeScript infers the return type, though it won't warn you about the runtime data loss.

Where it gets tricky is when you're cloning objects that have class instances or custom prototypes. TypeScript's type system says you have a User object, but at runtime you've got a plain object that's lost its methods. That mismatch between compile-time types and runtime reality is one of those things that keeps senior developers up at night.

If you're converting a JavaScript project and need proper type definitions for your data structures, SnipShift's JS to TS converter can generate interfaces from your actual runtime objects. It's useful for getting a starting point especially when the objects you're cloning have complex nested shapes.

The bottom line: structuredClone() has won. It took JavaScript way too long to get a native deep clone, but now that we have it, there's really no reason to reach for anything else unless you've got a specific edge case that demands it. Stop copying the JSON hack from Stack Overflow. Your future self will thank you.

For more JavaScript and TypeScript tips, check out SnipShift we've got 20+ free developer tools for converting and transforming code.

🔧

Try the JS to TypeScript

Paste your code, get the result instantly. AI-powered, free, no signup.

Open JS to TypeScript
Share:

You might also like