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
Aspect | structuredClone | JSON Parse/Stringify | Lodash cloneDeep | Custom Recursive |
---|---|---|---|---|
Performance | Good - native C++ | Fast but limited | Moderate - JS | Slowest - pure JS |
Type Support | Most types | Primitives only | All JS types | Customizable |
Browser Support | Modern only (2022+) | Universal | With library | Universal |
Circular Refs | Handles perfectly | Throws error | Handles well | Must implement |
Functions | Not supported | Lost | Preserved | Can preserve |
Special Objects | Dates, RegExp, etc | Lost | Most supported | Must 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.