Advanced JavaScriptFeatured

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.

By JavaScriptDoc Team
deep copycloningobjectsrecursiondata structures

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!