Deep Object Copy in JavaScript: Techniques, Pitfalls, and Patterns

Learn practical techniques for deep copying objects in JavaScript. Compare JSON cloning, structuredClone, and custom deep-copy strategies with edge-case guidance.

JavaScripting
JavaScripting Team
·5 min read
Quick AnswerDefinition

Deep object copy javascript means cloning an object and all of its nested objects so that the new structure has no shared references with the original. In practice you can use JSON-based cloning, structuredClone, or a custom recursive clone—each with trade-offs around functions, Dates, Maps, and prototypes. This guide details when to use which method and how to implement robust clones.

What deep object copy javascript means and why it matters

In JavaScript, objects are assigned by reference. A shallow copy duplicates only the top-level properties, leaving nested objects shared between the source and the copy. A truly deep object copy javascript clones every level, producing an independent tree. This matters for state management, undo/redo functionality, and avoiding accidental mutations. The most common approaches are JSON-based cloning, the modern structuredClone API, and custom recursive clones that handle special types like Date, RegExp, Map, and Set. Each approach has trade-offs: JSON cloning is simple but loses functions and prototypes; structuredClone covers many types but may not be available in all environments; custom clones require more code but offer maximum control.

JavaScript
// Shallow copy example (top-level only) const original = { a: 1, b: { c: 2 } }; const shallow = { ...original }; shallow.b.c = 99; console.log(original.b.c); // 99 (shared reference)
JavaScript
// JSON-based deep copy (loses functions, Dates, and prototypes) const deepJson = JSON.parse(JSON.stringify(original)); deepJson.b.c = 42;
JavaScript
// Modern deep copy when available const deepStruct = structuredClone(original); deepStruct.b.c = 7;

Tip: If you need to preserve class instances, prototypes, or circular references, JSON-based cloning is insufficient and a custom approach or structuredClone (where supported) is often preferable.

Shallow vs deep copy: mental model

To reason about copies, compare two objects:

JavaScript
const a = { n: 1, nested: { x: 2 } }; const shallow = Object.assign({}, a); shallow.n = 99; // safe: top-level change shallow.nested.x = 100; // unsafe: nested object is shared with a

A deep copy would duplicate nested objects as well:

JavaScript
const deep = JSON.parse(JSON.stringify(a)); deep.nested.x = 200; console.log(a.nested.x); // 2 (unchanged)

Keep in mind that the real-world decision depends on data shapes, performance constraints, and whether prototypes and class instances should be preserved.

Built-in options in modern JS: JSON, structuredClone, and fallbacks

If you only deal with plain data, JSON-based cloning is quick to write:

JavaScript
const original = { a: 1, b: { c: 2 } }; const clone = JSON.parse(JSON.stringify(original));

For broader data types, structuredClone is the modern solution when available:

JavaScript
const original = { a: 1, b: { c: 2 } }; const clone = structuredClone(original); clone.b.c = 3;

If structuredClone is not available, a safe fallback is to conditionally use JSON cloning or a custom clone. The following pattern selects the best available method at runtime:

JavaScript
function safeDeepCopy(obj) { if (typeof structuredClone === 'function') { return structuredClone(obj); } return JSON.parse(JSON.stringify(obj)); }

Caveat: JSON cloning cannot handle functions, undefined, Date becomes string, and loses class instances. Use structuredClone when cloning complex state or when your environment supports it.

Copying special types: Date, RegExp, Map, Set, and more

StructuredClone can clone many built-in types (Date, RegExp, Map, Set, ArrayBuffer, TypedArray), but JSON-based cloning cannot. See examples:

JavaScript
const obj = { date: new Date('2020-01-01'), regex: /ab+c/i, map: new Map([['key', { val: 1 }]]), set: new Set([1, 2, 3]) }; const copy = structuredClone(obj); copy.date.setHours(12); // works independently

If you implement a custom clone, add explicit support for these types:

JavaScript
function cloneSpecials(v, seen = new WeakMap()) { if (v === null || typeof v !== 'object') return v; if (seen.has(v)) return seen.get(v); if (v instanceof Date) return new Date(v); if (v instanceof RegExp) return new RegExp(v); if (v instanceof Map) { const m = new Map(); seen.set(v, m); v.forEach((val, key) => m.set(cloneSpecials(key, seen), cloneSpecials(val, seen))); return m; } if (v instanceof Set) { const s = new Set(); seen.set(v, s); v.forEach((val) => s.add(cloneSpecials(val, seen))); return s; } if (Array.isArray(v)) { const arr = []; seen.set(v, arr); v.forEach((e, i) => (arr[i] = cloneSpecials(e, seen))); return arr; } const out = {}; seen.set(v, out); for (const k in v) if (Object.prototype.hasOwnProperty.call(v, k)) out[k] = cloneSpecials(v[k], seen); return out; }

Note: Map and Set require explicit handling in custom clones. If you rely on JSON, these types will not survive a deep copy.

Custom deep copy function patterns: robust recursion with cycle handling

A robust deep copy must handle cycles, preserve certain types, and avoid infinite recursion. The following pattern uses a WeakMap to remember already-cloned objects, preventing infinite loops and ensuring proper sharing when requested:

JavaScript
function deepCopyWithCycles(value, seen = new WeakMap()) { if (value === null || typeof value !== 'object') return value; if (seen.has(value)) return seen.get(value); if (value instanceof Date) return new Date(value); if (value instanceof RegExp) return new RegExp(value); if (value instanceof Map) { const clone = new Map(); seen.set(value, clone); value.forEach((v, k) => clone.set(deepCopyWithCycles(k, seen), deepCopyWithCycles(v, seen))); return clone; } if (value instanceof Set) { const clone = new Set(); seen.set(value, clone); value.forEach((v) => clone.add(deepCopyWithCycles(v, seen))); return clone; } if (Array.isArray(value)) { const arr = []; seen.set(value, arr); value.forEach((v, i) => (arr[i] = deepCopyWithCycles(v, seen))); return arr; } const out = {}; seen.set(value, out); for (const key in value) { if (Object.prototype.hasOwnProperty.call(value, key)) { out[key] = deepCopyWithCycles(value[key], seen); } } return out; }

This pattern handles cycles, preserves Dates and RegExps, and can be extended to Map/Set. For extremely large graphs, consider streaming or incremental cloning to reduce peak memory usage.

Performance considerations and safety when cloning

Deep copying can be CPU-intensive and memory-hungry, especially for large, deeply nested structures. When you profile with real data, you may observe a noticeable impact on frame rates or UI responsiveness. A few practical rules:

  • Prefer structuredClone or a targeted clone for small to medium state trees constructed at runtime.
  • Reserve deep copies for state snapshots, undo buffers, or data you must isolate from the original.
  • Benchmark with representative data shapes, not synthetic extremes.
JavaScript
const t0 = performance.now(); const copy = deepCopyWithCycles(bigState); // or structuredClone(bigState) const t1 = performance.now(); console.log(`Copy took ${t1 - t0} ms`);

If performance is critical, an alternative is to normalize state into a serializable schema before copying, reducing the depth or flattening nested structures where possible.

Testing, validation, and common pitfalls

Thorough tests are essential for deep copies. Validate that nested objects are independent by mutating the copy and asserting the original remains unchanged. Also test with special values like Dates, Maps, Sets, and circular references. Common pitfalls include assuming JSON cloning preserves prototypes or class instances; remember that functions and Symbol values do not survive JSON cloning.

JavaScript
test('deepCopy should clone nested objects independently', () => { const original = { a: { b: { c: 1 } }, d: [1, 2, 3] }; const copy = deepCopyWithCycles(original); copy.a.b.c = 99; copy.d.push(4); expect(original.a.b.c).toBe(1); expect(original.d.length).toBe(3); });

For production code, integrate unit tests into your CI pipeline to detect regressions in cloning behavior as types evolve.

Step-by-step usage in a project: a practical roadmap

  1. Assess data shapes: Identify which objects require cloning and which types appear (Date, Map, Set, nested prototypes).
  2. Choose a strategy: Use structuredClone if available for most cases; fall back to a robust deepCopyWithCycles for complex graphs.
  3. Implement a baseline: Add a safeDeepCopy wrapper to centralize cloning decisions.
  4. Extend for edge cases: Add support for Maps, Sets, Dates, and RegExps in your custom function as needed.
  5. Validate with tests: Create unit tests for nested structures, circular references, and advanced types.
  6. Profile and optimize: Run performance tests and adjust cloning scope or data shape to meet your latency budgets.
  7. Review and secure: Ensure cloned data does not expose sensitive references in stateful applications.
  8. Document: Include usage examples and caveats in your repo docs.

Estimated time: 1-3 hours depending on data complexity and environment.

In most applications, you should avoid indiscriminate deep cloning of entire store objects. Use a targeted approach: clone only the parts of the state that truly require isolation, and prefer structuredClone when dealing with complex types. For plain JSON data, JSON.stringify/parse is concise but comes with limits. If your project involves custom classes, prototypes, or circular references, a custom deep copy with cycle handling is often warranted. Finally, always accompany cloning with tests, so changes to data shapes don’t silently introduce bugs.

Steps

Estimated time: 1-3 hours

  1. 1

    Assess data shapes

    List which properties and nested structures require copying. Note any Date, Map, Set, or circular references. This step informs the cloning strategy and helps avoid over-copying.

    Tip: Document the data graph to avoid surprises during refactors.
  2. 2

    Choose a strategy

    Decide between a built-in approach (structuredClone, JSON), or a custom clone that handles exotic types and cycles. Consider environment compatibility and performance needs.

    Tip: Prefer structuredClone when available for broad type support.
  3. 3

    Implement baseline clone

    Add a wrapper that selects the best available method and a core clone function for deep copies. Ensure tests cover nested objects.

    Tip: Keep cloning logic isolated to simplify maintenance.
  4. 4

    Handle special types

    Extend your clone to handle Date, RegExp, Map, and Set, either via structuredClone or a custom handler.

    Tip: Explicitly test Date/RegExp behavior to avoid silent data changes.
  5. 5

    Verify correctness

    Write unit tests validating independence, immutability, and edge cases like circular refs.

    Tip: Include tests for prototype-preserving scenarios if needed.
  6. 6

    Benchmark and optimize

    Profile cloning with realistic payloads; adjust scope and data layout to meet performance goals.

    Tip: Profile in the actual environment where the code runs.
Pro Tip: Prefer structuredClone for most use cases to avoid reinventing the wheel.
Warning: JSON cloning will drop functions, class instances, and prototypes; use it only for plain data.
Note: Test cloning with circular references to ensure your approach handles cycles gracefully.
Pro Tip: Benchmark cloning with realistic data to avoid over-optimizing for small, synthetic samples.

Prerequisites

Required

Optional

Keyboard Shortcuts

ActionShortcut
CopyCopy selected code or text in editorCtrl+C
PastePaste into editor or terminalCtrl+V
Comment blockToggle line commentsCtrl+/
Find in fileSearch within the current fileCtrl+F

Questions & Answers

What is deep copy in JavaScript and why is it necessary?

A deep copy duplicates an object and all nested objects, producing completely independent data. It is necessary when you need to mutate a copy without affecting the original structure, such as in state management or undo functionality.

A deep copy duplicates everything inside an object, so changes to the copy don’t change the original.

How does JSON-based cloning differ from structuredClone?

JSON-based cloning copies plain data only and strips functions, dates, and prototypes. structuredClone supports more types, including Date, RegExp, Map, and Set, but may not be available in all environments.

JSON cloning is simple but loses functions and classes; structuredClone is more robust but requires support.

Can I clone objects with prototypes or class instances?

Cloning with prototypes or class instances typically requires a custom clone that reconstructs the prototype chain or uses a serialization strategy that preserves constructors.

To clone classes or prototypes, you usually need a custom cloning approach.

Does structuredClone handle circular references?

StructuredClone can clone many complex structures, but circular references may require a custom solution or a well-designed wrapper around the cloning call.

Circular references can be tricky—structuredClone helps, but you may need a custom approach.

What should I test to ensure my copy is correct?

Test with nested objects, arrays, dates, maps, and sets. Mutate the copy and verify the original remains unchanged. Include circular references in tests if applicable.

Test that nested parts are independent and special types clone correctly.

What to Remember

  • Understand the difference between shallow and deep copies
  • Choose the cloning method that matches your data types
  • StructuredClone handles many built-in types reliably
  • JSON cloning is simple but limited
  • Always validate with tests for edge cases

Related Articles