Logo
Published on

Deep Copy Alternatives

How Deep Copy Alternatives Solve Complex Cloning

When spread operators and shallow copies fall short, developers need robust deep cloning solutions. From the native structuredClone API to battle-tested libraries like Lodash, each approach offers different trade-offs between performance, compatibility, and feature completeness for handling complex JavaScript data structures.

TL;DR

  • Native structuredClone() handles most cases elegantly
  • JSON methods work but lose types and functions
  • Lodash provides battle-tested deep cloning utilities
  • Recursive functions offer maximum control and customization
const deepCopy = structuredClone(originalObject)

The Deep Cloning Requirements

You're building a state management system that must preserve complete history snapshots for implementing undo/redo functionality across your application. Shallow copies break down because every state snapshot must be completely independent, handling nested objects, arrays, dates, and special objects without losing types.

// The incomplete JSON approach
const state = {
  timestamp: new Date(),
  user: { id: 1, name: 'Alice' },
  regex: /test/gi,
  handler: () => console.log('clicked'),
  items: new Set([1, 2, 3]),
}
// JSON loses types and functions
const jsonClone = JSON.parse(JSON.stringify(state))
console.log('Date type:', jsonClone.timestamp) // String!
console.log('Regex:', jsonClone.regex) // {}
console.log('Function:', jsonClone.handler) // undefined
console.log('Set:', jsonClone.items) // {} empty object

Modern structuredClone and battle-tested library solutions preserve complete data integrity across all types:

// The modern deep clone solutions
const state = {
  timestamp: new Date(),
  user: { id: 1, name: 'Alice' },
  nested: { deep: { value: 42 } },
  items: new Set([1, 2, 3]),
}
// Native structuredClone (2022+)
const cloned = structuredClone(state)
const isDate = cloned.timestamp instanceof Date
console.log('Date preserved:', isDate)
cloned.nested.deep.value = 100
console.log('Original:', state.nested.deep.value) // 42

Best Practises

Use deep cloning when:

  • ✅ Implementing undo/redo or history tracking features
  • ✅ Creating isolated test fixtures or mock data
  • ✅ Storing immutable snapshots of application state
  • ✅ Preventing any possibility of shared reference bugs

Avoid when:

  • 🚩 Working with huge objects where performance matters
  • 🚩 Objects contain DOM nodes or non-serializable items
  • 🚩 Circular references exist (except with structuredClone)
  • 🚩 Simple shallow copying would suffice for the use case

System Design Trade-offs

AspectstructuredCloneJSON Parse/StringifyLodash cloneDeepCustom Recursive
PerformanceGood - native C++Fast but limitedModerate - JSSlowest - pure JS
Type SupportMost typesPrimitives onlyAll JS typesCustomizable
Browser SupportModern only (2022+)UniversalWith libraryUniversal
Circular RefsHandles perfectlyThrows errorHandles wellMust implement
FunctionsNot supportedLostPreservedCan preserve
Special ObjectsDates, RegExp, etcLostMost supportedMust handle

More Code Examples

❌ Hacky clone attempts
// Traditional problematic deep clone approaches
function demonstrateCloneProblems() {
  const complexData = {
    id: 'abc-123',
    created: new Date('2024-01-15'),
    pattern: /[a-z]+/gi,
    calculate: function (x) {
      return x * 2
    },
    metadata: {
      tags: ['important', 'urgent'],
      author: {
        name: 'Developer',
        permissions: new Set(['read', 'write']),
      },
    },
    circular: null,
  }
  // Create circular reference
  complexData.circular = complexData
  // Attempt 1: JSON method fails
  try {
    const jsonClone = JSON.parse(JSON.stringify(complexData))
    console.log('JSON clone succeeded')
  } catch (error) {
    console.log('JSON failed:', error.message) // Circular structure
  }
  // Remove circular for JSON test
  delete complexData.circular
  const jsonClone = JSON.parse(JSON.stringify(complexData))
  const dateType = typeof jsonClone.created
  console.log('Date became:', dateType) // string
  console.log('Regex became:', jsonClone.pattern) // {}
  console.log('Function:', jsonClone.calculate) // undefined
  const perms = jsonClone.metadata.author && jsonClone.metadata.author.permissions
  console.log('Set became:', perms) // {}
  // Attempt 2: Object.assign still shallow
  const assignClone = Object.assign({}, complexData)
  assignClone.metadata.tags.push('modified')
  const origTags = complexData.metadata.tags
  console.log('Original tags:', origTags)
  const modified = complexData.metadata.tags.includes('modified')
  console.log('Tags modified:', modified)
  // Attempt 3: Spread operator also shallow
  const spreadClone = { ...complexData }
  spreadClone.metadata.author.name = 'Hacker'
  const author = complexData.metadata.author.name
  console.log('Original author:', author) // Hacker!
}
demonstrateCloneProblems()
✅ StructuredClone mastery
// Modern approaches for reliable deep cloning
function demonstrateModernCloning() {
  const complexData = {
    id: 'abc-123',
    created: new Date('2024-01-15'),
    metadata: {
      tags: ['important', 'urgent'],
      author: {
        name: 'Developer',
        permissions: new Set(['read', 'write']),
      },
      buffer: new Uint8Array([1, 2, 3, 4]),
    },
    circular: null,
  }
  // Add circular reference
  complexData.circular = complexData
  // Solution 1: structuredClone (best for modern browsers)
  if (typeof structuredClone !== 'undefined') {
    const modernClone = structuredClone(complexData)
    modernClone.metadata.author.name = 'Modified'
    console.log('Original author:', complexData.metadata.author.name)
    console.log('Clone author:', modernClone.metadata.author.name)
    console.log('Date preserved:', modernClone.created instanceof Date)
    const size = modernClone.metadata.author.permissions.size
    console.log('Set preserved:', size)
    console.log('Circular handled:', modernClone.circular === modernClone)
  }
  // Solution 2: Custom deep clone
  function deepClone(obj, h = new WeakMap()) {
    if (Object(obj) !== obj) return obj
    if (h.has(obj)) return h.get(obj)
    let r
    if (obj instanceof Date) r = new Date(obj)
    else if (obj instanceof Set) r = new Set([...obj].map((v) => deepClone(v, h)))
    else if (obj instanceof Array) r = obj.map((item) => deepClone(item, h))
    else if (obj.constructor) {
      r = new obj.constructor()
      h.set(obj, r)
      for (const k in obj) r[k] = deepClone(obj[k], h)
    }
    return r
  }
  delete complexData.circular
  const customClone = deepClone(complexData)
  customClone.metadata.tags.push('cloned')
  console.log('Original:', complexData.metadata.tags.length)
  console.log('Clone:', customClone.metadata.tags.length)
}
demonstrateModernCloning()

Technical Trivia

The Node.js v8.serialize breakthrough: Before structuredClone, Node.js developers discovered that v8.serialize/deserialize could deep clone objects faster than any JavaScript solution. This V8 engine feature, originally for worker threads, became a popular workaround until structuredClone arrived officially.

Why Lodash still matters: Despite native alternatives, Lodash's cloneDeep remains popular because it handles edge cases that structuredClone doesn't, like functions and undefined values. Many production systems still rely on Lodash for its predictable behavior across all JavaScript environments.

The MessageChannel hack: Before structuredClone, developers used MessageChannel postMessage to achieve deep cloning through the browser's structured cloning algorithm. This creative workaround leveraged the same internal mechanism that structuredClone now exposes directly.


Choose Your Deep Clone Strategy Wisely

Select structuredClone for modern applications needing robust cloning of standard objects and types. Fall back to JSON methods for simple data structures when universal compatibility matters. Adopt Lodash when you need functions preserved or work in legacy environments. Build custom solutions only when you need precise control over the cloning process or have unique requirements.