Understanding Deep Copy
Deep copying is one of those JavaScript concepts that seems simple until you encounter a nested object that mysteriously mutates across your application. Understanding deep copy isn't just academic--it's essential for building predictable, bug-free applications.
A deep copy of an object is a copy whose properties do not share the same references (point to the same underlying values) as those of the source object. This means when you change either the source or the copy, you can be assured you're not causing the other object to change too. Proper deep copy implementation is foundational to immutable state management practices that prevent hard-to-track bugs in complex applications.
The Shallow Copy Problem
Shallow copies only copy primitive values at the top level. When your object contains nested objects or arrays, these are not copied--they're shared between the original and the copy.
// Shallow copy with spread syntax
const original = { name: 'Apple', colors: ['red', 'green'] };
const shallow = { ...original };
// Modify nested array in the shallow copy
shallow.colors[0] = 'blue';
console.log(original.colors[0]); // 'blue' - Original is affected!
This shared reference behavior is the source of many bugs in JavaScript applications, especially in state management, form handling, and data transformation pipelines. Understanding the difference between shallow and deep copy is essential for any developer working with complex data structures.
1// Shallow copy creates shared nested references2const original = { items: [{ id: 1, name: 'Task 1' }] };3const shallowCopy = { ...original };4 5// Modify nested object in the copy6shallowCopy.items[0].completed = true;7 8// Original is also modified!9console.log(original.items[0].completed); // true10 11// Even top-level primitives are independent12shallowCopy.newProp = 'new';13console.log(original.newProp); // undefinedModern JavaScript: structuredClone()
The structuredClone() method is the modern standard for deep copying in JavaScript, available across all modern browsers since 2022. It creates a deep clone using the structured clone algorithm.
Key Advantages
- Handles circular references correctly
- Supports more data types including Error, Map, Set, typed arrays
- Native performance - faster than JSON serialization
- Transfer capability for efficient ArrayBuffer handling
- No external dependencies required
This native approach leverages browser-level optimizations that performance-focused development teams appreciate for eliminating external library dependencies while maintaining excellent runtime characteristics.
1// Basic deep copy2const original = { 3 name: 'John', 4 address: { city: 'NYC', zip: '10001' } 5};6const clone = structuredClone(original);7 8// Verify independence9console.log(clone === original); // false - different objects10console.log(clone.address === original.address); // false - nested also independent11 12// Circular reference handling (works!)13const obj = { name: 'circular' };14obj.self = obj;15const cloned = structuredClone(obj); // No error thrown16console.log(cloned.self === cloned); // true - circular reference preserved17 18// Transfer ArrayBuffers for memory efficiency19const buffer = new ArrayBuffer(16);20const uint8 = new Uint8Array(buffer);21uint8[0] = 42;22 23const transferred = structuredClone(uint8, { transfer: [buffer] });24console.log(uint8.byteLength); // 0 - buffer was transferred25console.log(transferred[0]); // 42 - data is available in cloned arrayThe JSON.parse(JSON.stringify()) Approach
This legacy approach serializes objects to JSON strings and parses them back, creating a deep copy in the process.
When It Works
- Plain objects with primitive values
- Arrays of primitives
- Simple nested structures
When It Fails
- Dates → become strings
- Functions → are lost
- undefined → properties are omitted
- Circular references → throw TypeError
- Symbols → are lost
- RegExp, Map, Set → become empty objects
While the JSON method is simple and dependency-free, its limitations make it suitable only for specific use cases where you know your data structure ahead of time.
1// Dates become strings2const withDate = { created: new Date('2025-01-01') };3const fromJson = JSON.parse(JSON.stringify(withDate));4console.log(typeof fromJson.created); // 'string', not Date!5 6// Functions are lost7const withFunc = { greet: () => 'hello', name: 'John' };8const noFuncs = JSON.parse(JSON.stringify(withFunc));9console.log(noFuncs.greet); // undefined10 11// undefined values are omitted12const withUndefined = { a: 1, b: undefined, c: 3 };13console.log(JSON.stringify(withUndefined)); // {"a":1,"c":3}14 15// Circular references throw16const circular = {};17circular.self = circular;18JSON.stringify(circular); // TypeError: cyclic object valueLodash cloneDeep: The Comprehensive Solution
For maximum compatibility and comprehensive type support, lodash's cloneDeep function handles virtually all JavaScript types, including those that structuredClone() cannot process.
Advantages
- Preserves property descriptors (getters/setters)
- Handles all built-in types correctly
- Circular reference support
- Maintains prototype chains
- Battle-tested and reliable
Trade-offs
- Adds external dependency
- Larger bundle size
- Slightly slower than native methods
For projects requiring robust JavaScript development across legacy and modern browsers, lodash provides the most reliable deep copy solution despite the bundle size trade-off.
1const _ = require('lodash');2 3// Complex object with various types4const complex = {5 date: new Date(),6 regex: /test/gi,7 map: new Map([['key', 'value']]),8 set: new Set([1, 2, 3]),9 nested: { deeper: { value: true } },10 fn: function greet() { return 'hello'; }11};12 13// All types preserved, including the function!14const cloned = _.cloneDeep(complex);15console.log(cloned.date instanceof Date); // true16console.log(cloned.regex instanceof RegExp); // true17console.log(cloned.map instanceof Map); // true18console.log(typeof cloned.fn); // 'function'19console.log(cloned.nested.deeper.value); // true20 21// Nested structure is fully independent22cloned.nested.deeper.value = false;23console.log(complex.nested.deeper.value); // true - original unchanged| Feature | structuredClone() | JSON.parse(JSON.stringify()) | lodash.cloneDeep |
|---|---|---|---|
| Circular references | Yes | No (throws) | Yes |
| Date objects | Preserved | Becomes string | Preserved |
| Functions | No (lost) | No (lost) | Preserved |
| undefined values | Preserved | Omitted | Preserved |
| Map/Set | Preserved | Empty object | Preserved |
| Getters/Setters | No (lost) | No (lost) | Preserved |
| Native performance | Yes | No (serialization) | No (library) |
| No dependencies | Yes | Yes | No |
Real-World Patterns
State Management (Redux-style)
// Create immutable state updates
function updateItem(state, itemId, changes) {
return structuredClone(state).map(item => {
if (item.id === itemId) {
return { ...item, ...changes };
}
return item;
});
}
API Response Caching
const cache = new Map();
async function fetchWithCache(url) {
if (cache.has(url)) {
return structuredClone(cache.get(url));
}
const response = await fetch(url);
const data = await response.json();
cache.set(url, structuredClone(data));
return data;
}
Form Draft Preservation
function saveFormDraft(formState) {
const draft = structuredClone(formState);
draft.savedAt = new Date().toISOString();
draft.status = 'draft';
localStorage.setItem('formDraft', JSON.stringify(draft));
return draft;
}
These patterns are essential for building robust applications that handle data predictably, whether you're working with client-side state management or server-side data processing.
Choose the right deep copy method for your use case
Use structuredClone() by Default
Native, fast, and handles most modern JavaScript types. Your go-to choice for new code.
JSON Methods for Simple Data
Only when you know your data is JSON-safe and doesn't contain dates, functions, or circular refs.
Lodash for Maximum Compatibility
When you need to clone functions, preserve getters/setters, or support legacy browsers.
Test Edge Cases
Always verify your deep copy handles circular references, special types, and large data structures.