Deep Clone Objects in JavaScript: Beyond JSON.parse
Compare deep clone strategies including JSON.parse, structuredClone, manual recursion, and library approaches for copying nested objects with circular references and special types
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Deep Clone Objects in JavaScript: Beyond JSON.parse
Copy nested JavaScript objects without shared references using modern and legacy approaches. This recipe compares JSON.parse, structuredClone, manual recursive cloning, and library solutions while handling edge cases like circular references, functions, and special object types.
When to Use This
- State management requires immutable updates without mutating original data
- API responses are cached and must not be modified by consumers
- Configuration objects are passed to multiple modules that may modify them
Solution
1. JSON.parse Approach (Limited)
// clones/JsonClone.ts
function jsonClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
// Works for plain objects and arrays
const original = { a: 1, b: { c: 2 } };
const copy = jsonClone(original);
// Limitations
jsonClone({ date: new Date() }); // Date becomes string
jsonClone({ map: new Map() }); // Map becomes {}
jsonClone({ fn: () => 1 }); // Function becomes undefined
jsonClone({ a: {} }); copy.a = original; // Circular: throws
2. structuredClone (Modern Browsers and Node 17+)
// clones/StructuredClone.ts
function modernClone<T>(obj: T): T {
return structuredClone(obj);
}
// Supports more types
const original = {
date: new Date(),
map: new Map([['key', 'value']]),
set: new Set([1, 2, 3]),
arrayBuffer: new Uint8Array([1, 2, 3]).buffer,
nested: { a: 1 },
};
const copy = modernClone(original);
// Limitations
modernClone({ fn: () => 1 }); // Function throws
modernClone({ el: document.body }); // DOM nodes throw
3. Manual Recursive Clone
// clones/RecursiveClone.ts
function deepClone<T>(obj: T, cache = new WeakMap<object, unknown>()): T {
// Handle primitives and null
if (obj === null || typeof obj !== 'object') {
return obj;
}
// Handle circular references
if (cache.has(obj)) {
return cache.get(obj) as T;
}
// Handle Date
if (obj instanceof Date) {
return new Date(obj.getTime()) as unknown as T;
}
// Handle Array
if (Array.isArray(obj)) {
const copy: unknown[] = [];
cache.set(obj, copy);
obj.forEach((item, index) => {
copy[index] = deepClone(item, cache);
});
return copy as unknown as T;
}
// Handle Object
const copy = Object.create(Object.getPrototypeOf(obj));
cache.set(obj, copy);
Object.entries(obj as Record<string, unknown>).forEach(([key, value]) => {
copy[key] = deepClone(value, cache);
});
return copy;
}
4. Library-Based Cloning
// clones/LibraryClone.ts
import cloneDeep from 'lodash/cloneDeep';
import { klona } from 'klona';
// Lodash: battle-tested, handles most cases
const lodashCopy = cloneDeep(original);
// Klona: smaller, faster, modern alternative
const klonaCopy = klona(original);
// Comparison
const obj = {
date: new Date(),
regex: /test/gi,
nested: { a: 1 },
};
// All produce independent copies
lodashCopy.nested.a = 2; // obj.nested.a still 1
klonaCopy.nested.a = 3; // obj.nested.a still 1
5. Performance Comparison
// benchmarks/cloneBench.ts
const largeObject = {
users: Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `User ${i}`,
metadata: { created: new Date(), tags: ['a', 'b'] },
})),
};
// Results for 1000 iterations (approximate):
// JSON.parse: ~50ms (fastest but limited)
// structuredClone: ~80ms (native, no functions)
// klona: ~120ms (compact, modern)
// lodash: ~200ms (most robust)
// recursive: ~250ms (customizable)
How It Works
- JSON.parse serializes to string then parses, stripping non-JSON types
- structuredClone is a native API that supports more types but still excludes functions
- Recursive cloning traverses properties, preserving prototype chains and handling circular refs
- Libraries optimize hot paths and handle edge cases like descriptors and symbols
Production Considerations
- Use
structuredClonein modern environments for native performance - Prefer
klonaoverlodashif bundle size matters - For React state, consider Immer for structural sharing instead of full cloning
Common Mistakes
- Using
JSON.parsefor objects containing Dates, Maps, or functions - Spreading nested objects (
{ ...obj }) which only shallow-clones the first level - Not handling circular references, causing stack overflow in recursive solutions
FAQ
Q: Is const copy = { ...original } a deep clone?
A: No. It creates a shallow copy. Nested objects are still shared references.
Q: Can I deep clone class instances?
A: structuredClone strips methods. Use manual recursion or libraries that preserve prototypes.