DEV Community

Cover image for How to Get a Perfect Deep Copy in JavaScript?
Zachary Lee
Zachary Lee

Posted on • Originally published at on

How to Get a Perfect Deep Copy in JavaScript?

Originally published in my newsletter.


In JavaScript, data types can be categorized into primitive value types and reference value types. The main difference between these types lies in how they are handled and copied in memory.

For primitive value types (such as undefined, null, number, string, boolean, symbol, and bigint), JavaScript uses a pass-by-value method for copying. This means that when a primitive value is assigned to another variable, a copy of the value is actually created. Therefore, if the original variable is modified, the copied variable will not be affected. The following code demonstrates this:

let primitiveValue = 1;
const copyPrimitiveValue = primitiveValue;

primitiveValue = 2;
console.log('primitiveValue: ', primitiveValue); // Outputs 2
console.log('copyPrimitiveValue: ', copyPrimitiveValue); // Outputs 1
Enter fullscreen mode Exit fullscreen mode

For reference value types, such as objects, arrays, and functions, JavaScript uses a pass-by-reference method. When copying a reference value, what is actually copied is a reference to the object, not a copy of the object itself. This means that if any variable modifies the properties of the object, all variables that reference the object will reflect this change. For example:

const referenceValue = { value: 1 };
const copyReferenceValue = referenceValue;

referenceValue.value = 2;
console.log('referenceValue: ', referenceValue); // Outputs { value: 2 }
console.log('copyReferenceValue: ', copyReferenceValue); // Outputs { value: 2 }
Enter fullscreen mode Exit fullscreen mode

By this method, JavaScript ensures the independence of primitive values and the connectivity of reference values, making data operations predictable and consistent.

Shallow copy

A shallow copy means that only one layer of the object is copied, and the deep layer of the object directly copies an address. There are many native methods in Javascript that are shallow copies. For example, using Object.assign API or the spread operator.

const target = {};
const source = { a: { b: 1 }, c: 2 };
Object.assign(target, source);

source.a.b = 3;
source.c = 4;

console.log(source); // { a: { b: 3 }, c: 4 }
console.log(target); // { a: { b: 3 }, c: 2 }

// Same effect as Object.assign
const target1 = { ...source };
Enter fullscreen mode Exit fullscreen mode

Deep copy

Deep copy means cloning two identical objects but without any connection to each other.

1. JSON.stringify API

const source = { a: { b: 1 } };
const target = JSON.parse(JSON.stringify(source));

source.a.b = 2;
console.log(source); // { a: { b: 2 } };
console.log(target); // { a: { b: 1 } };
Enter fullscreen mode Exit fullscreen mode

Well, it seems that JSON.stringify can implement deep copying, but it has some defects. For example, it cannot copy functions, undefined, Date, cannot copy non-enumerable properties, cannot copy circularly referenced objects, and so on. You can check out the detailed description on MDN.

2. structuredClone API

I've found that there's already a native API called structuredClone, designed specifically for this purpose. It creates a deep clone of a given value using the structured clone algorithm. This means Function objects cannot be cloned and will throw a DataCloneError exception. It also doesn't clone setters, getters, and similar metadata functionalities.

3. Almost perfect deep copy

const deepClone = (obj, map = new WeakMap()) => {
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);

  if (map.has(obj)) {
    return map.get(obj);

  const allDesc = Object.getOwnPropertyDescriptors(obj);
  const cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc);

  map.set(obj, cloneObj);

  for (const key of Reflect.ownKeys(obj)) {
    const value = obj[key];

    cloneObj[key] =
      value instanceof Object && typeof value !== 'function'
        ? deepClone(value, map)
        : value;

  return cloneObj;
Enter fullscreen mode Exit fullscreen mode

The above code is the final result, let me explain how it came from.

  1. First, we use WeakMap as a hash table to solve the circular reference problem, which can effectively prevent memory leaks. You can check the description of WeakMap on MDN.

  2. For the special types Date and RegExp, a new instance is directly generated and returned.

  3. Use Object.getOwnPropertyDescriptors to get all property descriptions of the current object, and use Object.getPrototypeOfto get the prototype of the current object. Passing these two items as arguments to Object.create API to create a new object with the same prototype and the same properties.

  4. Use Reflect.ownKeys to iterate over all properties of the current object, including non-enumerable properties and Symbol properties, as well as normal properties. In this way, the deep-seated value can be continuously copied into the current new object in the loop and recursion.

  5. In the loop judgment, except that the function is directly assigned, the others are re-copied by recursion.

Next, we can use the test code to verify.

const symbolKey = Symbol('symbolKey');

const originValue = {
  num: 0,
  str: '',
  boolean: true,
  unf: void 0,
  nul: null,
  obj: { name: 'object', id: 1 },
  arr: [0, 1, 2],
  func() {
  date: new Date(0),
  reg: new RegExp('/regexp/ig'),
  [symbolKey]: 'symbol',

Object.defineProperty(originValue, 'innumerable', {
  // writable is true to ensure that the assignment operator can be used
  writable: true,
  enumerable: false,
  value: 'innumerable',

// Create circular reference
originValue.loop = originValue;

// Deep Copy
const clonedValue = deepClone(originValue);

// Change original value
originValue.arr.push(3); = 'newObject';

// Remove circular reference
originValue.loop = '';
originValue[symbolKey] = 'newSymbol';

console.log('originValue: ', originValue);
console.log('clonedValue: ', clonedValue);
Enter fullscreen mode Exit fullscreen mode

Image description

Great, it looks like it's working well.

If you find this helpful, please consider subscribing to my newsletter for more insights on web development. Thank you for reading!

Top comments (1)

silverium profile image
Soldeplata Saketos

Does this work fine with recursion objects referencing each other?

const obj = { a: obj.b, b: obj.a }
Enter fullscreen mode Exit fullscreen mode

(this is pseudocode but you get the gist)