Methods For Deep Cloning Objects In Javascript

Master deep cloning techniques from structuredClone() to Lodash. Create independent object copies, avoid mutation bugs, and choose the right approach for modern JavaScript applications.

Why Shallow Copies Fall Short

A shallow copy creates a new object, but only copies references to nested structures rather than creating new instances of them. This means nested arrays and objects share memory with the original, causing unexpected mutations when you modify the copy.

In JavaScript, objects are assigned and passed by reference, not by value. When you use the spread operator (...) or Object.assign() to copy an object, you create a new top-level object--but any nested objects within it still point to the same memory locations as the original. This behavior often surprises developers who expect complete isolation.

const original = {
 name: 'User Profile',
 settings: {
 theme: 'dark',
 notifications: true
 }
};

// Shallow copy with spread operator
const shallowCopy = { ...original };

// Modify nested property
shallowCopy.settings.theme = 'light';

// Original is affected!
console.log(original.settings.theme); // 'light' - unexpected!
console.log(shallowCopy.settings.theme); // 'light'

As shown above, modifying shallowCopy.settings.theme also changes original.settings.theme because both share the same settings object reference. This reference problem becomes especially problematic in state management scenarios.

The Reference Problem in Real Applications

In state management--whether updating React component state, Redux stores, or Vue reactive objects--accidental mutations can trigger hard-to-debug issues. A component might re-render unexpectedly, or a computed property might return stale data. Deep cloning solves this by creating entirely independent copies at every level of nesting.

A deep copy creates a new object and copies all properties from the original object, recursively creating new instances for nested objects rather than sharing references.

The Modern Solution: structuredClone()

The structuredClone() function is a native browser API that creates true deep clones using the structured clone algorithm. Introduced in 2022 and now available across all modern browsers, it handles a wide range of JavaScript types including Maps, Sets, Dates, RegExp, ArrayBuffers, and circular references.

The function takes the value to clone as its first argument and optionally accepts a transfer map as the second argument. It returns a deep clone of the original value, throwing a DataCloneError if any value cannot be serialized.

const clone = structuredClone(value);
const cloneWithTransfer = structuredClone(value, { transfer: [buffer] });

The key advantages of structuredClone() include native browser support with no dependencies, comprehensive type handling including complex data structures, automatic circular reference resolution, and optional transfer semantics for performance optimization with large binary data.

Supported Types

Unlike JSON-based approaches, structuredClone() supports numerous JavaScript types that the JSON serialization process cannot handle. This includes Date objects, which preserve their type rather than becoming strings, as well as Map and Set instances that maintain their internal data structures.

The supported types include all primitives, Arrays, Objects, Date, RegExp, Map, Set, ArrayBuffer, TypedArrays, and objects with prototype chains (preserved via internal slot copying).

structuredClone preserves JavaScript types
1const original = {2 date: new Date('2025-01-01'),3 map: new Map([['key', 'value']]),4 set: new Set([1, 2, 3]),5 regex: /test/gi6};7 8const clone = structuredClone(original);9console.log(clone.date instanceof Date); // true - type preserved10console.log(clone.map instanceof Map); // true11console.log(clone.set instanceof Set); // true

Handling Circular References

Circular references--where an object references itself directly or through a chain of properties--break most cloning approaches. structuredClone() handles these automatically, preserving the circular structure in the clone without causing infinite loops or errors.

const circular = { name: 'Circular Object' };
circular.self = circular;

const cloned = structuredClone(circular);
console.log(cloned.self === circular); // false - new reference
console.log(cloned.self === cloned); // true - circular preserved

This automatic handling of circular references eliminates the need for custom detection logic or manual break-and-repair approaches that were common with older cloning methods.

Transferable Objects for Performance

The structuredClone() function accepts an optional transfer array that moves ownership of ArrayBuffers and other transferable objects rather than copying them. This can significantly improve performance when cloning large binary data, as the data is simply transferred rather than duplicated in memory.

const buffer = new ArrayBuffer(1024 * 1024 * 16); // 16MB
const original = { buffer };

const cloned = structuredClone(original, {
 transfer: [buffer]
});

console.log(original.buffer.byteLength); // 0 - transferred, not cloned
console.log(cloned.buffer.byteLength); // 16777216

After transfer, the original buffer becomes zero-length because ownership has moved to the clone. This is particularly useful when working with WebGL textures, large datasets, or file data in modern web applications.

Transferring ArrayBuffers for performance
1const buffer = new ArrayBuffer(1024 * 1024 * 16); // 16MB2const original = { buffer };3 4const cloned = structuredClone(original, {5 transfer: [buffer]6});7 8console.log(original.buffer.byteLength); // 0 - transferred, not cloned9console.log(cloned.buffer.byteLength); // 16777216

Browser Compatibility and Fallbacks

structuredClone() is widely supported in Chrome, Firefox, Safari, and Edge since March 2022. It achieved Baseline status in 2023, indicating universal support in current browser versions. For older browser support, consider the core-js polyfill or fall back to alternative methods.

// Feature detection
function canUseStructuredClone() {
 try {
 structuredClone({ test: true });
 return true;
 } catch (e) {
 return false;
 }
}

// Fallback for older browsers
function deepClone(value) {
 if (canUseStructuredClone()) {
 return structuredClone(value);
 }
 return JSON.parse(JSON.stringify(value));
}

The Legacy Approach: JSON.stringify/parse

Before structuredClone(), developers relied on JSON serialization for deep cloning. While simple and universally compatible, this approach has significant limitations that make it unsuitable for many modern applications that work with complex JavaScript data structures.

The approach involves converting an object to a JSON string with JSON.stringify(), then parsing that string back to a JavaScript object with JSON.parse(). This creates entirely new object instances at every level, breaking all references. The simplicity makes it appealing for basic use cases, but the type limitations can cause subtle bugs.

Understanding how JavaScript objects and references work is essential for choosing the right cloning strategy for your application.

JSON serialization approach
1const original = {2 name: 'Example',3 nested: {4 value: 42,5 items: [1, 2, 3]6 }7};8 9const clone = JSON.parse(JSON.stringify(original));10clone.nested.value = 100;11console.log(original.nested.value); // 42 - unaffected

Significant Limitations

Several JavaScript types cannot survive JSON serialization. Functions are lost entirely, becoming undefined in the result. Symbols disappear. Date objects become ISO strings. undefined values are omitted from the output. RegExp objects become empty plain objects. Map, Set, WeakMap, and WeakSet instances are converted to empty objects.

The JSON serialization approach only works with serializable types and loses type information for anything that doesn't map directly to JSON values. This makes it unsuitable for objects containing functions, Dates, or complex JavaScript-specific types--exactly the kinds of objects commonly found in modern React applications and Node.js backends.

const problematic = {
 func: () => 'removed',
 symbol: Symbol('lost'),
 date: new Date(),
 undefined: undefined,
 regex: /test/,
 map: new Map([['a', 1]]),
 set: new Set([1, 2])
};

const cloned = JSON.parse(JSON.stringify(problematic));
console.log(cloned.func); // undefined
console.log(cloned.symbol); // undefined
console.log(cloned.date); // ISO string, not Date
console.log(cloned.undefined); // property removed
console.log(cloned.regex); // {}
console.log(cloned.map); // {}
console.log(cloned.set); // {}
JSON serialization loses types and functions
1const problematic = {2 func: () => 'removed',3 symbol: Symbol('lost'),4 date: new Date(),5 undefined: undefined,6 regex: /test/,7 map: new Map([['a', 1]]),8 set: new Set([1, 2])9};10 11const cloned = JSON.parse(JSON.stringify(problematic));12console.log(cloned.func); // undefined13console.log(cloned.symbol); // undefined14console.log(cloned.date); // ISO string, not Date15console.log(cloned.undefined); // property removed16console.log(cloned.regex); // {}17console.log(cloned.map); // {}18console.log(cloned.set); // {}

When JSON Serialization Is Appropriate

Despite its limitations, JSON.stringify/parse remains useful for simple data objects containing only primitive values, strings, arrays, and plain objects. It's ideal for configuration objects, API responses with known schemas, and scenarios where maximum browser compatibility is required.

The JSON approach works well when you know your data structure consists only of JSON-serializable types, when you're working with external APIs that exchange JSON data, or when you need to support ancient browsers and structuredClone() isn't available. For complex applications with diverse data types, however, structuredClone() is the superior choice.

Third-Party Libraries: Lodash cloneDeep

Lodash's cloneDeep function provides comprehensive deep cloning with full type preservation and no size limits. While structuredClone() covers most use cases, certain scenarios still benefit from Lodash's approach.

The library handles every JavaScript type, including those that structuredClone() cannot serialize. This includes functions, symbols, and custom class instances with their prototype chain intact. For projects already using Lodash, the additional bundle size may be acceptable given the comprehensive functionality.

If you're working with complex JavaScript applications, understanding how to properly clone data structures is essential for building maintainable applications with predictable state management.

Lodash cloneDeep preserves custom classes
1class User {2 constructor(name) {3 this.name = name;4 this.greet = () => `Hello, ${this.name}`;5 }6}7 8const original = new User('Alice');9const clone = _.cloneDeep(original);10 11console.log(clone instanceof User); // true12console.log(clone.greet()); // Hello, Alice13console.log(clone === original); // false14console.log(clone.greet === original.greet); // false

Complete Type Preservation

Lodash cloneDeep handles every JavaScript type, including those that structuredClone() cannot serialize. This includes functions, symbols, and custom class instances with their prototype chain intact. The cloned object maintains the same constructor and prototype as the original.

Comparing Bundle Size Impact

Importing the entire Lodash library adds significant bundle size--approximately 70KB minified. For modern applications built with bundlers like Vite or webpack, consider using lodash-es for tree-shakable imports or importing only the specific function needed.

// Full import - not recommended for simple cloning needs
import _ from 'lodash';

// Tree-shakable import (recommended)
import cloneDeep from 'lodash/cloneDeep';

// ES modules with bundler support
import { cloneDeep } from 'lodash-es';

Using lodash-es allows modern bundlers to eliminate unused code, keeping your application bundle lean while still providing access to cloneDeep when needed. This approach is particularly valuable for performance-optimized web applications where bundle size directly impacts loading times.

When to Use Lodash Over Native Methods

Choose Lodash cloneDeep when you need to clone objects containing functions, symbols, or custom class instances. Also consider it for projects already using Lodash where bundle size impact is already accepted. For most other cases, structuredClone() provides equivalent functionality with zero dependencies.

Performance Comparison and Best Practices

Deep cloning performance varies significantly between methods depending on object size, nesting depth, and the types of values being cloned. Understanding these differences helps choose the right approach for performance-critical applications.

Performance Characteristics

structuredClone() generally performs well for large objects with nested structures because it's implemented natively in the browser's JavaScript engine. JSON.stringify/parse has serialization overhead that grows with object size and complexity--each property must be visited twice (once for stringify, once for parse). Lodash cloneDeep is well-optimized but adds the overhead of a JavaScript function call and recursive traversal implemented in JavaScript rather than native code.

For most applications, the performance difference between methods is negligible for typical use cases with objects containing hundreds rather than thousands of properties. Focus on correctness first, then optimize based on actual profiling data from your specific use case using browser developer tools.

Choosing the Right Method

Start with structuredClone() as your default choice. It handles most use cases natively, has no dependencies, and is optimized by browser vendors. Fall back to JSON.stringify/parse only when you need maximum browser compatibility and your data is JSON-serializable. Use Lodash cloneDeep when you must clone objects containing functions, symbols, or custom class instances.

Avoiding Common Pitfalls

Be aware that deep cloning has inherent costs proportional to object size and depth. Consider whether you truly need a complete copy or if a more targeted approach would work--for example, only cloning the specific nested object that needs modification rather than an entire large state tree. For immutable data patterns common in React applications, consider using libraries like Immer that allow "mutating" a draft state while producing immutable results, avoiding full deep copies in many cases.

When working with very large objects or performance-critical code paths, profile different approaches to find the optimal solution for your specific use case. The best method depends on your data characteristics, browser requirements, and bundle size constraints. For developers working with React hooks and state management, understanding when to use deep cloning versus other immutability patterns is crucial for optimal application performance.

Deep Cloning Methods at a Glance

structuredClone()

Native browser API since 2022. Supports Maps, Sets, Dates, circular references, and transferable objects. No dependencies required.

JSON.stringify/parse

Universal compatibility. Simple but limited--loses functions, symbols, Dates become strings. Best for simple data objects.

Lodash cloneDeep

Complete type preservation including functions and custom classes. Slightly larger bundle but comprehensive support.

Frequently Asked Questions

What is the difference between shallow and deep cloning?

Shallow copy creates a new object but only copies top-level properties. Nested objects remain as references, so modifications to nested properties affect both the original and the copy. Deep copy creates entirely new instances at every level, resulting in completely independent objects.

Can structuredClone() clone functions?

No, structuredClone() cannot clone functions or other non-serializable types like Symbols. These will throw a DataCloneError. Use Lodash cloneDeep if you need to clone objects containing functions.

What are transferable objects?

Transferable objects (like ArrayBuffer) can have their ownership transferred rather than copied. This is more efficient for large binary data. The original becomes unusable after transfer.

Is deep cloning expensive for performance?

Yes, deep cloning has computational cost proportional to object size and depth. For large objects, consider whether you truly need a complete copy or if a more targeted approach would work.

Build Robust JavaScript Applications

Our team specializes in modern JavaScript development, from React applications to Node.js backends. Let us help you implement clean, maintainable patterns.