How to Get a Perfect Deep Copy in JavaScript?
Tags: javascript, webdev, frontend, programming
Published: 2024-05-24 00:24:23

*Originally published in* [*my newsletter*](https://webdeveloper.beehiiv.com/p/get-perfect-deep-copy-javascript).

# Pre-knowledge

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:

```javascript
let primitiveValue = 1;
const copyPrimitiveValue = primitiveValue;
​
primitiveValue = 2;
console.log('primitiveValue: ', primitiveValue); // Outputs 2
console.log('copyPrimitiveValue: ', copyPrimitiveValue); // Outputs 1
```

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:

```javascript
const referenceValue = { value: 1 };
const copyReferenceValue = referenceValue;
​
referenceValue.value = 2;
console.log('referenceValue: ', referenceValue); // Outputs { value: 2 }
console.log('copyReferenceValue: ', copyReferenceValue); // Outputs { value: 2 }
```

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.

```javascript
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 };
```

# Deep copy

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

**1. JSON.stringify API**

```javascript
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 } };
```

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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify?utm_source=webdeveloper.beehiiv.com&utm_medium=newsletter&utm_campaign=how-to-get-a-perfect-deep-copy-in-javascript#description)_.

**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](https://developer.mozilla.org/en-US/docs/Glossary/Deep_copy?utm_source=webdeveloper.beehiiv.com&utm_medium=newsletter&utm_campaign=how-to-get-a-perfect-deep-copy-in-javascript) of a given value using the [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm?utm_source=webdeveloper.beehiiv.com&utm_medium=newsletter&utm_campaign=how-to-get-a-perfect-deep-copy-in-javascript). 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**

```javascript
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;
};
```

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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap?utm_source=webdeveloper.beehiiv.com&utm_medium=newsletter&utm_campaign=how-to-get-a-perfect-deep-copy-in-javascript)_.

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.getPrototypeOf`to 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.

```javascript
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() {
    console.log('function');
  },
  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);
originValue.obj.name = 'newObject';

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

console.log('originValue: ', originValue);
console.log('clonedValue: ', clonedValue);
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vo0392gtmti518rdoola.png)


Great, it looks like it's working well.

* * *

*If you find this helpful, [**please consider subscribing**](https://webdeveloper.beehiiv.com/) to my newsletter for more insights on web development. Thank you for reading!*