JavaScript Deep Copy: Complete Implementation Guide
Master deep copying in JavaScript. Learn various methods to create true copies of nested objects and arrays, handle edge cases, and implement custom solutions.
JavaScript Deep Copy: Complete Implementation Guide
Deep copying creates a completely independent copy of an object or array, including all nested objects and arrays. Unlike shallow copying, changes to the copy don't affect the original, and vice versa. This guide covers various methods and considerations for implementing deep copies in JavaScript.
Understanding Deep Copy
A deep copy recursively copies all nested objects and arrays, creating entirely new instances at every level. This ensures complete independence between the original and the copy.
// Demonstrating deep copy behavior
const original = {
name: 'John',
age: 30,
address: {
city: 'New York',
coordinates: {
lat: 40.7128,
lng: -74.006,
},
},
hobbies: ['reading', 'gaming'],
};
// True deep copy (using structuredClone)
const deepCopy = structuredClone(original);
// Modifying nested objects
deepCopy.address.city = 'Los Angeles';
deepCopy.address.coordinates.lat = 34.0522;
deepCopy.hobbies.push('cooking');
// Original remains unchanged
console.log(original.address.city); // 'New York'
console.log(original.address.coordinates.lat); // 40.7128
console.log(original.hobbies); // ['reading', 'gaming']
// All references are different
console.log(original.address === deepCopy.address); // false
console.log(original.address.coordinates === deepCopy.address.coordinates); // false
console.log(original.hobbies === deepCopy.hobbies); // false
Deep Copy Methods
1. structuredClone() (Modern Approach)
The structuredClone()
method is the newest and most robust way to create deep copies.
// Basic usage
const original = {
date: new Date(),
regex: /pattern/gi,
nested: {
array: [1, 2, { deep: true }],
set: new Set([1, 2, 3]),
map: new Map([['key', 'value']]),
},
};
const cloned = structuredClone(original);
// Supported types
const supportedTypes = {
// Primitives
string: 'text',
number: 42,
boolean: true,
null: null,
undefined: undefined,
bigint: 123n,
// Objects
object: { nested: true },
array: [1, 2, 3],
// Built-in objects
date: new Date(),
regexp: /pattern/gi,
map: new Map([['a', 1]]),
set: new Set([1, 2, 3]),
arrayBuffer: new ArrayBuffer(8),
typedArray: new Int32Array([1, 2, 3]),
// Error objects
error: new Error('Test error'),
typeError: new TypeError('Type error'),
// Other
blob: new Blob(['data'], { type: 'text/plain' }),
file: new File(['content'], 'file.txt', { type: 'text/plain' }),
};
try {
const clonedTypes = structuredClone(supportedTypes);
console.log('All types cloned successfully');
} catch (error) {
console.error('Clone error:', error);
}
// Limitations - these will throw errors
const unsupported = {
function: () => {}, // Functions not supported
symbol: Symbol('sym'), // Symbols not supported
weakMap: new WeakMap(), // WeakMap not supported
weakSet: new WeakSet(), // WeakSet not supported
domElement: document.createElement('div'), // DOM nodes not supported
};
// Handling circular references
const circular = { name: 'Object' };
circular.self = circular;
const clonedCircular = structuredClone(circular);
console.log(clonedCircular.self === clonedCircular); // true (maintains circular reference)
2. JSON Methods (Limited but Common)
Using JSON.parse(JSON.stringify())
is a common but limited approach.
// Basic JSON deep copy
const original = {
name: 'User',
data: {
scores: [100, 200, 300],
metadata: {
created: '2024-01-01',
},
},
};
const jsonCopy = JSON.parse(JSON.stringify(original));
// Works for JSON-compatible data
jsonCopy.data.scores.push(400);
console.log(original.data.scores); // [100, 200, 300] (unchanged)
// Limitations of JSON approach
const complexObject = {
// These work fine
string: 'text',
number: 123,
boolean: true,
null: null,
array: [1, 2, 3],
object: { nested: true },
// These don't work
undefined: undefined, // Becomes: missing
function: () => 'hello', // Becomes: missing
symbol: Symbol('sym'), // Becomes: missing
date: new Date('2024-01-01'), // Becomes: string "2024-01-01T00:00:00.000Z"
regex: /pattern/g, // Becomes: empty object {}
infinity: Infinity, // Becomes: null
nan: NaN, // Becomes: null
map: new Map([['a', 1]]), // Becomes: empty object {}
set: new Set([1, 2, 3]), // Becomes: empty object {}
};
const jsonClone = JSON.parse(JSON.stringify(complexObject));
console.log(jsonClone);
// Missing: undefined, function, symbol
// Converted: date (string), regex ({}), Infinity (null), NaN (null), Map ({}), Set ({})
// Custom JSON serialization
const customSerialize = {
date: new Date(),
toJSON() {
return {
date: this.date.toISOString(),
_type: 'CustomObject',
};
},
};
console.log(JSON.parse(JSON.stringify(customSerialize)));
// { date: '2024-...', _type: 'CustomObject' }
3. Recursive Implementation
Creating a custom deep copy function gives you full control.
// Basic recursive deep copy
function deepCopy(obj, visited = new WeakMap()) {
// Handle primitives and null
if (obj === null || typeof obj !== 'object') {
return obj;
}
// Handle circular references
if (visited.has(obj)) {
return visited.get(obj);
}
// Handle Date
if (obj instanceof Date) {
return new Date(obj.getTime());
}
// Handle Array
if (Array.isArray(obj)) {
const copy = [];
visited.set(obj, copy);
obj.forEach((item, index) => {
copy[index] = deepCopy(item, visited);
});
return copy;
}
// Handle RegExp
if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
}
// Handle Map
if (obj instanceof Map) {
const copy = new Map();
visited.set(obj, copy);
obj.forEach((value, key) => {
copy.set(deepCopy(key, visited), deepCopy(value, visited));
});
return copy;
}
// Handle Set
if (obj instanceof Set) {
const copy = new Set();
visited.set(obj, copy);
obj.forEach((value) => {
copy.add(deepCopy(value, visited));
});
return copy;
}
// Handle Objects
const copy = {};
visited.set(obj, copy);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key], visited);
}
}
return copy;
}
// Test the function
const test = {
num: 42,
str: 'hello',
date: new Date(),
regex: /test/gi,
arr: [1, 2, { nested: true }],
map: new Map([['key', { value: 'data' }]]),
set: new Set([1, 2, 3]),
};
// Add circular reference
test.circular = test;
const copied = deepCopy(test);
console.log(copied.circular === copied); // true (circular reference maintained)
console.log(copied.arr[2] === test.arr[2]); // false (deep copied)
4. Advanced Deep Copy Implementation
A more comprehensive implementation handling edge cases.
function advancedDeepCopy(obj, options = {}) {
const {
includeNonEnumerable = false,
includeSymbols = false,
copyPrototype = false,
customHandler = null,
} = options;
const visited = new WeakMap();
function copy(target) {
// Primitives
if (target === null || typeof target !== 'object') {
return target;
}
// Circular reference check
if (visited.has(target)) {
return visited.get(target);
}
// Custom handler
if (customHandler) {
const handled = customHandler(target);
if (handled !== undefined) {
return handled;
}
}
// Built-in objects
if (target instanceof Date) {
return new Date(target.getTime());
}
if (target instanceof RegExp) {
const regex = new RegExp(target.source, target.flags);
regex.lastIndex = target.lastIndex;
return regex;
}
if (target instanceof Map) {
const map = new Map();
visited.set(target, map);
target.forEach((value, key) => {
map.set(copy(key), copy(value));
});
return map;
}
if (target instanceof Set) {
const set = new Set();
visited.set(target, set);
target.forEach((value) => {
set.add(copy(value));
});
return set;
}
if (target instanceof Error) {
const error = new target.constructor(target.message);
error.stack = target.stack;
visited.set(target, error);
Object.keys(target).forEach((key) => {
error[key] = copy(target[key]);
});
return error;
}
if (ArrayBuffer.isView(target)) {
const TypedArray = target.constructor;
const buffer = target.buffer.slice(0);
return new TypedArray(buffer, target.byteOffset, target.length);
}
if (target instanceof ArrayBuffer) {
return target.slice(0);
}
// Arrays
if (Array.isArray(target)) {
const arr = new Array(target.length);
visited.set(target, arr);
target.forEach((item, index) => {
arr[index] = copy(item);
});
return arr;
}
// Objects
const cloned = copyPrototype
? Object.create(Object.getPrototypeOf(target))
: {};
visited.set(target, cloned);
// Get all property keys
let keys = Object.keys(target);
if (includeNonEnumerable) {
keys = Object.getOwnPropertyNames(target);
}
if (includeSymbols) {
const symbols = Object.getOwnPropertySymbols(target);
keys = keys.concat(symbols);
}
// Copy properties with descriptors
keys.forEach((key) => {
const descriptor = Object.getOwnPropertyDescriptor(target, key);
if (descriptor) {
if ('value' in descriptor) {
descriptor.value = copy(descriptor.value);
}
Object.defineProperty(cloned, key, descriptor);
}
});
return cloned;
}
return copy(obj);
}
// Example usage with advanced features
const complexObject = {
enumerable: 'visible',
[Symbol('sym')]: 'symbol value',
nested: {
date: new Date(),
error: new Error('Test'),
buffer: new ArrayBuffer(8),
typedArray: new Int32Array([1, 2, 3]),
},
};
// Add non-enumerable property
Object.defineProperty(complexObject, 'hidden', {
value: 'non-enumerable',
enumerable: false,
});
// Add getter/setter
Object.defineProperty(complexObject, 'computed', {
get() {
return this.enumerable.toUpperCase();
},
set(value) {
this.enumerable = value.toLowerCase();
},
});
const fullCopy = advancedDeepCopy(complexObject, {
includeNonEnumerable: true,
includeSymbols: true,
copyPrototype: true,
});
console.log(Object.getOwnPropertyDescriptor(fullCopy, 'hidden'));
console.log(fullCopy.computed); // 'VISIBLE'
Handling Special Cases
1. Circular References
// Circular reference handling
function handleCircularReferences() {
const obj = {
name: 'Parent',
children: [],
};
const child = {
name: 'Child',
parent: obj,
};
obj.children.push(child);
obj.self = obj;
// Using structuredClone
const cloned1 = structuredClone(obj);
console.log(cloned1.self === cloned1); // true
console.log(cloned1.children[0].parent === cloned1); // true
// Using custom function with WeakMap
const cloned2 = deepCopy(obj);
console.log(cloned2.self === cloned2); // true
console.log(cloned2.children[0].parent === cloned2); // true
}
// Detect circular references
function hasCircularReference(obj, seen = new WeakSet()) {
if (obj === null || typeof obj !== 'object') {
return false;
}
if (seen.has(obj)) {
return true;
}
seen.add(obj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (hasCircularReference(obj[key], seen)) {
return true;
}
}
}
seen.delete(obj);
return false;
}
2. Custom Classes
// Deep copying custom classes
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
this.id = Symbol('id');
}
greet() {
return `Hello, I'm ${this.name}`;
}
clone() {
const cloned = new Person(this.name, this.age);
cloned.id = Symbol('id'); // New symbol
return cloned;
}
}
class Team {
constructor(name) {
this.name = name;
this.members = [];
}
addMember(person) {
this.members.push(person);
}
clone() {
const cloned = new Team(this.name);
cloned.members = this.members.map((member) =>
member.clone ? member.clone() : deepCopy(member)
);
return cloned;
}
}
// Custom deep copy with class support
function deepCopyWithClasses(obj, visited = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (visited.has(obj)) {
return visited.get(obj);
}
// Check for clone method
if (typeof obj.clone === 'function') {
const cloned = obj.clone();
visited.set(obj, cloned);
return cloned;
}
// Continue with regular deep copy
return deepCopy(obj, visited);
}
// Usage
const team = new Team('Dev Team');
team.addMember(new Person('Alice', 30));
team.addMember(new Person('Bob', 25));
const clonedTeam = deepCopyWithClasses(team);
clonedTeam.members[0].name = 'Alicia';
console.log(team.members[0].name); // 'Alice' (unchanged)
console.log(clonedTeam.members[0].greet()); // "Hello, I'm Alicia"
3. DOM Elements
// Deep copying DOM elements
function cloneDOM(element, deep = true) {
if (!(element instanceof Node)) {
throw new TypeError('Expected a DOM Node');
}
const cloned = element.cloneNode(deep);
// Clone event listeners (requires tracking)
if (element._eventListeners) {
element._eventListeners.forEach(({ type, listener, options }) => {
cloned.addEventListener(type, listener, options);
});
}
// Clone custom properties
if (element._customData) {
cloned._customData = deepCopy(element._customData);
}
return cloned;
}
// Enhanced addEventListener for tracking
function trackEventListeners(element) {
const original = element.addEventListener;
element._eventListeners = [];
element.addEventListener = function (type, listener, options) {
this._eventListeners.push({ type, listener, options });
original.call(this, type, listener, options);
};
}
// Usage
const div = document.createElement('div');
trackEventListeners(div);
div.innerHTML = '<p>Hello World</p>';
div._customData = { id: 123, metadata: { created: new Date() } };
div.addEventListener('click', () => console.log('Clicked!'));
const clonedDiv = cloneDOM(div);
document.body.appendChild(clonedDiv); // Clicking will log 'Clicked!'
Performance Optimization
1. Lazy Deep Copy
// Lazy deep copy using Proxy
function lazyDeepCopy(obj) {
const copied = new WeakMap();
function createProxy(target) {
if (target === null || typeof target !== 'object') {
return target;
}
if (copied.has(target)) {
return copied.get(target);
}
const handler = {
get(target, prop) {
const value = target[prop];
if (value !== null && typeof value === 'object') {
return createProxy(value);
}
return value;
},
set(target, prop, value) {
// Copy on write
if (!copied.has(target)) {
const copy = Array.isArray(target) ? [...target] : { ...target };
copied.set(target, copy);
return Reflect.set(copy, prop, value);
}
return Reflect.set(copied.get(target), prop, value);
},
};
const proxy = new Proxy(target, handler);
return proxy;
}
return createProxy(obj);
}
// Usage - copies only when modified
const original = {
large: new Array(1000000).fill(0),
nested: {
deep: {
value: 'unchanged',
},
},
};
const lazy = lazyDeepCopy(original);
// No actual copying yet
lazy.nested.deep.value = 'changed'; // Only this branch is copied
console.log(original.nested.deep.value); // 'unchanged'
2. Selective Deep Copy
// Deep copy with path selection
function selectiveDeepCopy(obj, paths) {
const pathSet = new Set(paths);
function shouldDeepCopy(currentPath) {
return paths.some(
(path) => path.startsWith(currentPath) || currentPath.startsWith(path)
);
}
function copy(target, path = '') {
if (target === null || typeof target !== 'object') {
return target;
}
if (Array.isArray(target)) {
return target.map((item, index) => {
const itemPath = `${path}[${index}]`;
return shouldDeepCopy(itemPath) ? copy(item, itemPath) : item;
});
}
const result = {};
for (const key in target) {
if (target.hasOwnProperty(key)) {
const keyPath = path ? `${path}.${key}` : key;
result[key] = shouldDeepCopy(keyPath)
? copy(target[key], keyPath)
: target[key];
}
}
return result;
}
return copy(obj);
}
// Usage
const data = {
user: {
profile: {
name: 'John',
avatar: { url: 'http://...' },
},
settings: {
theme: 'dark',
notifications: { email: true },
},
},
cache: {
large: new Array(10000).fill(0),
},
};
// Only deep copy user.profile
const partial = selectiveDeepCopy(data, ['user.profile']);
partial.user.profile.name = 'Jane';
console.log(data.user.profile.name); // 'John'
console.log(data.cache === partial.cache); // true (shared reference)
3. Benchmark Different Methods
// Performance comparison
function benchmarkDeepCopy() {
// Create test object
const createTestObject = (depth, breadth) => {
if (depth === 0) return Math.random();
const obj = {};
for (let i = 0; i < breadth; i++) {
obj[`key${i}`] = createTestObject(depth - 1, breadth);
}
return obj;
};
const testObj = createTestObject(5, 5); // 5 levels deep, 5 properties each
// Benchmark functions
const methods = {
'JSON method': (obj) => JSON.parse(JSON.stringify(obj)),
structuredClone: (obj) => structuredClone(obj),
'Custom recursive': (obj) => deepCopy(obj),
'Advanced custom': (obj) => advancedDeepCopy(obj),
};
// Run benchmarks
Object.entries(methods).forEach(([name, method]) => {
try {
const start = performance.now();
for (let i = 0; i < 1000; i++) {
method(testObj);
}
const end = performance.now();
console.log(`${name}: ${(end - start).toFixed(2)}ms`);
} catch (error) {
console.log(`${name}: Error - ${error.message}`);
}
});
}
Best Practices
1. Choose the Right Method
// Decision tree for deep copy method
function chooseDeepCopyMethod(obj) {
// For modern environments with basic needs
if (typeof structuredClone !== 'undefined') {
try {
return structuredClone(obj);
} catch (e) {
// Fall through if unsupported types
}
}
// For JSON-compatible data only
if (isJSONCompatible(obj)) {
return JSON.parse(JSON.stringify(obj));
}
// For complex objects with special types
return advancedDeepCopy(obj);
}
function isJSONCompatible(obj) {
// Simple check - could be more thorough
try {
JSON.stringify(obj);
return true;
} catch {
return false;
}
}
2. Error Handling
// Safe deep copy with error handling
function safeDeepCopy(obj, fallbackValue = null) {
try {
// Try structuredClone first
if (typeof structuredClone !== 'undefined') {
return structuredClone(obj);
}
} catch (e) {
console.warn('structuredClone failed:', e.message);
}
try {
// Fallback to custom implementation
return deepCopy(obj);
} catch (e) {
console.error('Deep copy failed:', e);
return fallbackValue;
}
}
// With validation
function validateAndCopy(obj, schema) {
// Validate object structure
if (!validateSchema(obj, schema)) {
throw new Error('Object does not match schema');
}
return safeDeepCopy(obj);
}
3. Testing Deep Copy
// Test utilities for deep copy
const DeepCopyTester = {
// Test independence
testIndependence(original, copy) {
const paths = this.getAllPaths(original);
paths.forEach((path) => {
const originalValue = this.getByPath(original, path);
const copyValue = this.getByPath(copy, path);
if (typeof originalValue === 'object' && originalValue !== null) {
console.assert(
originalValue !== copyValue,
`Reference shared at path: ${path}`
);
}
});
},
// Get all paths in object
getAllPaths(obj, prefix = '') {
const paths = [];
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const path = prefix ? `${prefix}.${key}` : key;
paths.push(path);
if (typeof obj[key] === 'object' && obj[key] !== null) {
paths.push(...this.getAllPaths(obj[key], path));
}
}
}
return paths;
},
// Get value by path
getByPath(obj, path) {
return path.split('.').reduce((curr, key) => curr?.[key], obj);
},
// Test completeness
testCompleteness(original, copy) {
const originalStr = JSON.stringify(original, null, 2);
const copyStr = JSON.stringify(copy, null, 2);
console.assert(originalStr === copyStr, 'Copy is not complete or accurate');
},
};
// Usage
const original = {
a: 1,
b: { c: 2, d: { e: 3 } },
};
const copy = deepCopy(original);
DeepCopyTester.testIndependence(original, copy);
DeepCopyTester.testCompleteness(original, copy);
Common Pitfalls and Solutions
1. Memory Issues
// Memory-efficient deep copy for large objects
function* deepCopyIterative(obj) {
const stack = [{ source: obj, target: {}, path: '' }];
const visited = new WeakMap();
while (stack.length > 0) {
const { source, target, path } = stack.pop();
for (const key in source) {
if (source.hasOwnProperty(key)) {
const value = source[key];
if (value === null || typeof value !== 'object') {
target[key] = value;
} else if (visited.has(value)) {
target[key] = visited.get(value);
} else {
if (Array.isArray(value)) {
target[key] = [];
} else {
target[key] = {};
}
visited.set(value, target[key]);
stack.push({
source: value,
target: target[key],
path: `${path}.${key}`,
});
}
yield { path: `${path}.${key}`, progress: stack.length };
}
}
}
return target;
}
// Usage with progress tracking
const iterator = deepCopyIterative(largeObject);
let result;
while (!(result = iterator.next()).done) {
console.log(`Processing: ${result.value.path}`);
}
2. Prototype Chain
// Deep copy with prototype chain preservation
function deepCopyWithPrototype(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
const prototype = Object.getPrototypeOf(obj);
const copy = Object.create(prototype);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopyWithPrototype(obj[key]);
}
}
return copy;
}
// Test
class CustomClass {
constructor(value) {
this.value = value;
}
getValue() {
return this.value;
}
}
const instance = new CustomClass(42);
const copied = deepCopyWithPrototype(instance);
console.log(copied instanceof CustomClass); // true
console.log(copied.getValue()); // 42
Conclusion
Deep copying in JavaScript requires careful consideration of:
- Data types present in your objects
- Performance requirements for your application
- Memory constraints with large objects
- Special cases like circular references
- Browser/environment support for methods
Key takeaways:
- Use
structuredClone()
for modern environments - JSON methods work for simple, JSON-compatible data
- Custom implementations offer the most control
- Always handle circular references
- Test thoroughly with your specific data structures
Best practices:
- Start with
structuredClone()
when available - Fall back to custom implementations for unsupported types
- Use WeakMap for circular reference handling
- Consider lazy copying for performance
- Always validate deep copy completeness
Choose the right deep copy method based on your specific needs, and always test with representative data to ensure correctness and performance!