Deep Clone With Vanilla JS

Narek Keryan
Webtips
Published in
4 min readJun 30, 2020
Image by https://pixabay.com/users/aitoff-388338/

In JavaScript, we can have two types of variables: primitive types and reference types.

A primitive type variable is data that is not an object and has no methods. The latest ECMAScript standard defines 7 primitive types: number, string, boolean, undefined, null, symbol, bigint. When we create a variable of primitive type, value is directly assigned to that variable.

Primitive type variable contains exactly the value

Reference types work differently. When we create variables of reference type: objects, value is not directly assigned to that variable, instead a reference (location in memory) of the value is assigned to it.

Reference type variable contains address where the value is stored

In other words, a value of primitive type is the actual value. A value of reference type is a reference to another value.

So, how does this affect to cloning?

If we try to copy the reference type variable by assigning it to another variable, we will end up creating another reference to the same entity. This means that modifying original will also affect cloned versions and vice versa.

Copying reference type variable does not actually copy the value of a variable, instead it copies the address where the value is stored. So, changing the property of the copied variable will also change it inside the original one.

So, how this problem can be solved?

One of the simplest solutions would be to create a new object and assign properties separately. With ES6+ what we can do is to use spread operator.

const starWarsCharacter = { name: 'Han Solo', species: 'Human' };
const otherStarWarsCharacter = { ...starWarsCharacter };
otherStarWarsCharacter.name = 'Chewbacca';
otherStarWarsCharacter.species = 'Wookiee';
console.log(starWarsCharacter.name); // Han Solo
console.log(otherStarWarsCharacter.species); // Wookiee

At first glance, this approach solves our problem. But does this actually solve it?

No.

If we try to clone object which is not as simple as this one and has nested objects inside, we will end up cloning references of this nested objects.

To solve this problem as well, we should dynamically iterate over all properties and check if the property is of primitive or reference type. If it’s a primitive type then we can simply clone it and if it’s a reference type, we should use recursion to iterate over its properties until all nested primitive properties are taken into consideration.

function cloneDeep(entity) {
return /Array|Object/.test(Object.prototype.toString.call(entity))
? Object.assign(new entity.constructor, ...Object.keys(entity).map((prop) => ({ [prop]: cloneDeep(entity[prop]) })))
: entity;
}
const obj1 = { a: 1, b: { c: 2, d: [ 3, 4, { e: 5 } ] } };
const obj2 = cloneDeep(obj1);
obj2.b.d[2].e = 6;
console.log(obj1.b.d[2].e); // 5
console.log(obj2.b.d[2].e); // 6

It seems like the issue is solved with nested objects, but it’s not a final solution yet. There is a case when this will throw an error. This is when we have circular references.

const obj1 = {};
obj1.ref = obj1;
const obj2 = cloneDeep(obj1); // Uncaught RangeError: Maximum call stack size exceeded

How we can proceed now? To solve this error as well we should modify our function. We will use WeakMap data structure to cache references.

function cloneDeep(entity, cache = new WeakMap) {
const referenceTypes = ['Array', 'Object'];
const entityType = Object.prototype.toString.call(entity);
if (!new RegExp(referenceTypes.join('|')).test(entityType)) return entity; if (cache.has(entity)) {
return cache.get(entity);
}
const c = new entity.constructor; cache.set(entity, c); return Object.assign(c, ...Object.keys(entity).map((prop) => ({ [prop]: cloneDeep(entity[prop], cache) })));
}

With this function, we solved the issue with circular references. But what if we have more complex data structures. Let’s modify our function, so it could also support Map, Set, Date. So, our final function will look like this.

function cloneDeep(entity, cache = new WeakMap) {
const referenceTypes = ['Array', 'Object', 'Map', 'Set', 'Date'];
const entityType = Object.prototype.toString.call(entity);
if (
!new RegExp(referenceTypes.join('|')).test(entityType) ||
entity instanceof WeakMap ||
entity instanceof WeakSet
) return entity;
if (cache.has(entity)) {
return cache.get(entity);
}
const c = new entity.constructor;

if (entity instanceof Map) {
entity.forEach((value, key) => c.set(cloneDeep(key), cloneDeep(value)));
}
if (entity instanceof Set) {
entity.forEach((value) => c.add(cloneDeep(value)));
}
if (entity instanceof Date) {
return new Date(entity);
}
cache.set(entity, c); return Object.assign(c, ...Object.keys(entity).map((prop) => ({ [prop]: cloneDeep(entity[prop], cache) })));
}

Conclusion

Copying reference type variable does not actually copy the value of a variable, instead it copies the address where the value is stored. To copy the value of reference type variable we should iterate over all properties and copy them separately.

You can use this function to achieve deep cloning or create your own or use some well-tested utility libraries which have this function integrated.

--

--