How to Get a Perfect Deep Copy in JavaScript

Achieve almost perfect deep copy of about 20 lines

Zachary Lee
JavaScript in Plain English

--

Originally published in my newsletter.

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:

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:

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.

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

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.

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

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() {
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);

Great, it looks like it’s working well.

If you found this helpful, please consider subscribing for more insights on web development. Thank you for reading!

In Plain English 🚀

Thank you for being a part of the In Plain English community! Before you go:

--

--