Deep clone an object

A deep clone copies every level of a value, so changing the copy can never affect the original. Return primitives as they’re, map over arrays recursively, and rebuild objects key by key. The recursion is the whole answer; the interesting part is what the naive approaches get wrong.

Easy recursion object references

The question

Usually opens with "how would you deep clone this object", and the first real question is whether you reach for JSON.parse(JSON.stringify(value)). That answer isn’t wrong so much as incomplete, and saying why is most of the marks.

The solution

Primitives are returned untouched, which also terminates the recursion. Everything else is rebuilt one level at a time.

function deepClone(value) {
  if (value === null || typeof value !== "object") return value;

  if (Array.isArray(value)) {
    return value.map((item) => deepClone(item));
  }

  const copy = {};
  for (const key of Object.keys(value)) {
    copy[key] = deepClone(value[key]);
  }
  return copy;
}

How it works

Primitives are the base case. They’re immutable, so returning them directly is both correct and what stops the recursion. null gets checked before typeof, for the usual reason. Arrays are handled separately from objects so an array clones to an array rather than to an object with numeric keys, and every value recurses, which is the part that makes it deep rather than shallow.

The tests it has to pass

This is the part most explanations leave out. "It works" is not a claim you can check by reading. These are the 2 cases Practice Pad runs against your solution, and the implementation above passes all of them.

Easy2 cases · independence and equality

copies nested values so the clone can change alone

const original = { user: { name: "Ada", skills: ["JavaScript"] } };
const cloned = deepClone(original);
cloned.user.skills.push("TypeScript");
expectEqual(original.user.skills, ["JavaScript"]);
expectEqual(cloned.user.skills, ["JavaScript", "TypeScript"]);

returns an equal but separate object

const original = { id: 1, tags: ["a"] };
const cloned = deepClone(original);
expectEqual(cloned, original);
expectEqual(cloned === original, false);
expectEqual(cloned.tags === original.tags, false);

Edge cases to watch out for

Reaching for JSON.parse(JSON.stringify(value))

It works for plain JSON-shaped data and fails everywhere else: Date becomes a string, Map, Set and RegExp become {}, undefined and functions vanish, NaN and Infinity become null, and circular references throw. Worth saying out loud as a fast option with known limits.

Confusing shallow with deep

{ ...original } and Object.assign({}, original) copy the top level only. Nested objects are still shared, so mutating clone.user.name changes the original. The first test is built to catch exactly this.

Treating arrays as plain objects

Rebuilding an array by iterating its keys produces { "0": ..., "1": ... }, which is no longer an array and breaks every array method the caller might use.

Ignoring circular references

A self-referencing object sends the recursion into an infinite loop and blows the stack. Tracking visited objects in a WeakMap and returning the existing copy on a second visit is the standard fix.

Modern answer worth mentioning: structuredClone() is built into every current browser and Node, and it handles Date, Map, Set, ArrayBuffer and circular references. It can’t clone functions or DOM nodes. Interviewers still ask for the manual version, but knowing the built-in exists is a point in your favour.

Follow up questions

How do you clone a Date, Map or Set?

Each needs its own branch before the generic object case, because none of them expose their contents as own enumerable keys and all three would otherwise clone to {}. Construct a new one from the original: new Date(value), new Map(value), new Set(value), recursing into the entries if the clone has to be deep.

Why is JSON.parse(JSON.stringify(obj)) a bad deep clone?

It silently changes your data. Dates become strings, Maps and Sets become empty objects, undefined values and functions are dropped, NaN and Infinity become null, and circular references throw. It’s fine for plain JSON-shaped data and dangerous as a habit.

Can structuredClone replace a manual deep clone?

For most purposes, yes. It’s built into modern browsers and Node, handles Date, Map, Set, RegExp, typed arrays and circular references, and is faster than the JSON trick. It throws on functions, DOM nodes and symbols.

Practise this with the tests running

Practice Pad runs these 2 cases against your own solution, in a real editor, on your Mac. Twenty-four questions, and an optional AI panel that explains why a test failed.

Download for macOS Free. Requires macOS 12 or later on Apple Silicon.

See all 24 questions