back to series

Implement a deepClone function

A clone is not a formally defined term in JavaScript, but in general it is well understood as a copy of a value.

In JavaScript copies can be of two kind i.e, copy by value and copy by reference. All non-primitive types are copies by references meaning any update on them will reflect in any copies at the same times.

Let’s take a small example before tackling the problem statement.

./index.js
let msg1 = 'Hello World';
let msg2 = msg1;
msg2 = 'Hello World 2';
// Yields: 'Hello World', 'Hello World 2'
console.log(msg1, msg2);
let obj1 = {
a: 2,
b: {
c: 3
}
};
let obj2 = obj1;
delete obj1.a;
delete obj1.b.c;
/**
* Yields same for both
* {
"b": {}
}
*/
console.log(obj1, obj2);

In above example:

  1. Updating the value of msg2, does not affect msg1 since they were copied by value.
  2. Updating the value of obj1, did not affect obj2 since they were copied by reference, i.e they hold same reference on the memory.
Variation: Basic implementation

1. Basic implementation

We have a implement a utility called deepClone, which can create copy of a value by removing any references on them. Consider only JSON serializable values as input.

./src/deepClone.js
function deepClone(value) {
if (typeof value !== 'object') {
return value;
}
if (Object.is(null, value)) {
return value;
}
if (Array.isArray(value)) {
const resultArr = [];
value.forEach((item) => {
resultArr.push(deepClone(item));
});
return resultArr;
}
const resultObj = Object.create({});
for (const key in valueObj) {
if (Object.hasOwn(valueObj, key)) {
const value = valueObj[key];
if (typeof value !== 'object') {
resultObj[key] = value;
} else {
resultObj[key] = deepClone(value);
}
}
}
return resultObj;
}
// Usage
let obj1 = {
a: 2,
b: {
c: 3
}
};
let obj2 = deepClone(obj1);
delete obj1.a;
delete obj1.b.c;
/**
* Yields:
* {
"b": {}
}
*/
console.log(obj1);
/**
* Yields: {
"b": {
"c": 3
}
}
*/
console.log(obj2);
Variation: All Data Types

2. All Data Types implementation

In the above implementation we also considered JSON serializable values. In this particular case we need to clone all possible data types in JavaScript as much as possible mainly: Date, Map, Set, Symbol, RegExp and BigInt

The implementation remains almost the same here, we just need to handle the new cases explicitly.

./src/deepClone.js
import getTypeof from './src/utils.js';
function deepClone(input) {
// this utility is covered below
const argType = getTypeof(input);
switch (argType) {
// the primitives/values which don't store reference
// function can be treated with same reference here (as only exception)
case 'boolean':
case 'string':
case 'bigint':
case 'number':
case 'null':
case 'undefined':
case 'symbol':
case 'function': {
return input;
}
case 'date': {
return new Date(input);
}
case 'regexp': {
return new RegExp(input);
}
// we copy each item as clone and return a new set
case 'set': {
const clonedSet = new Set();
input.forEach((item) => {
clonedSet.add(deepClone(item));
});
return clonedSet;
}
// we copy each item as clone and return a new map
case 'map': {
const clonedMap = new Map();
input.forEach((item, key) => {
clonedMap.set(key, deepClone(item));
});
return clonedMap;
}
case 'array': {
const clonedArr = [];
input.forEach((item) => {
clonedArr.push(deepClone(item));
});
return clonedArr;
}
case 'object': {
// so we even clone the prototype of the object input
const clonedObject = Object.create(Object.getPrototypeOf(input));
// we are using Relect.ownKeys since it's the only way symbols on object
for (const key of Reflect.ownKeys(input)) {
const value = input[key];
clonedObject[key] = deepClone(value);
}
return clonedObject;
}
}
}
// Usage
const mapData = new Map();
mapData.set('key1', 'msg1');
mapData.set('key2', new Set([3, 4, 5]));
const set = new Set([1, 2, 3]);
const date = new Date();
const symbol = Symbol('s');
const obj = {
msg: 'Hello',
[symbol]: 'Hi Symbol'
};
const arr = [1, 2];
const fn = (data) => `Hello ${data}`;
const regex = new RegExp(/^hel/);
let obj1 = {
a: 0,
b: 1,
c: null,
d: undefined,
e: arr,
f: obj,
g: set,
h: mapData,
i: date,
j: regex,
symbol: 'symbol',
k: fn
};
const obj2 = deepClone(obj1);
delete obj1.a;
obj1.e.push(1, 2, 3);
obj1.g.add(10);
console.log('Obj1', obj1);
console.log('Obj2', obj2); // not affected by above changes on obj1

You may have noticed a utility function called getTypeof which I used above. As the typeof operator is unreliable here, the new utility helps us get the actual type of input correctly as a string.

./src/utils.js
function getTypeof(input) {
const argType = typeof input;
if (argType === 'object') {
if (Array.isArray(input)) {
return 'array';
}
if (input instanceof Map) {
return 'map';
}
if (input instanceof Set) {
return 'set';
}
if (input instanceof RegExp) {
return 'regexp';
}
if (input instanceof Date) {
return 'date';
}
if (Object.is(null, input)) {
return 'null';
}
return 'object';
}
return argType;
}

Further Reading

I strongly encourage you to explore and tackle additional questions in my Recursion Questions for Frontend Interviews blog series.

By doing so, you can enhance your understanding and proficiency with recursion, preparing you to excel in your upcoming frontend interviews.

Wishing you best. Happy Interviewing 🫡.