Implement Deep Clone using vanilla javascript

This is yet another common question asked in many of the interviews. This tests the fundamentals of javascript, recursion, object references, and so on...

During the interview, you must clarify what is the scope of the cloning that is expected. If the question is to clone any given object, array, prototype chains, or primitives, then doing it recursively (or) iterating every key is the best option.

NOTE: However, if it is enough just to clone primitives, you could go with JSON.stringify

NOTE: You could also use libraries like lodash to achieve the same but since we are looking to implement it from the scratch, we will not use lodash in this article.

NOTE: You can use spread (...) operators but again you would have to do it recursively as it only clones at a single level and doesn't work for nested levels by default.

What's deep cloning in javascript?

Deep cloning is the process of creating a new object with the same properties and values as another object. However, in deep cloning, all the properties are cloned recursively, so that even nested objects are completely cloned and not just referenced. In JavaScript, deep cloning is important when you want to create a completely new object that is not dependent on the original object.

What are the common use cases of deep cloning?

Below are some of the real-world use cases where it is quite helpful:

  1. Caching: In order to cache data, you may want to make a deep copy of an object, so that the original object is not modified.

  2. Undo/Redo functionality: If you want to implement an undo/redo functionality in your app, you need to create a copy of the original object, and deep cloning is the best way to do that.

  3. State management in React: In React, you need to maintain state data in order to re-render components. If you use deep cloning, you can ensure that state data is not mutated, and it is easier to debug any issues.

  4. Object-oriented programming: In object-oriented programming, you may want to create a new object with the same properties as an existing object, but you don't want them to be references.

  5. Copying configuration data: If you want to copy configuration data from one object to another, you need to use deep cloning to ensure that all the properties are copied correctly.

Implementation

Let's see how to implement this functionality.

Cloning only primitives?

You can rely on JSON.stringify.

function deepClonePrimitives(obj) {
  return JSON.parse(JSON.stringify(obj));
}

However, the above code would fail in case you have objects, functions, and other prototype objects. So, in order to clone everything effectively, we could use recursion.

Cloning everything?

Again, here we need to understand what is the scope of cloning we are looking for. You can clarify this with the interviewer talking about a few things you can support considering the time of the interview. Let's say we support the following:

  1. Date objects,

  2. Functions,

  3. Primitives,

  4. Any level Arrays,

  5. Any level javascript objects, ...

function deepClone(obj, cache = new WeakMap()) {
  if (obj === null || typeof obj !== "object") {
    return obj;
  }

  if (obj instanceof Date) {
    return new Date(obj.getTime());
  }

  if (cache.has(obj)) {
    return cache.get(obj);
  }

  const clonedObj = Object.create(
    Object.getPrototypeOf(obj),
    Object.getOwnPropertyDescriptors(obj)
  );
  cache.set(obj, clonedObj);

  const allProperties = [
    ...Object.getOwnPropertyNames(obj),
    ...Object.getOwnPropertySymbols(obj)
  ];

  for (const prop of allProperties) {
    const descriptor = Object.getOwnPropertyDescriptor(obj, prop);

    if (descriptor.configurable) {
      descriptor.value = deepClone(descriptor.value, cache);
    }

    Object.defineProperty(clonedObj, prop, descriptor);
  }

  return clonedObj;
}

The function takes two arguments: the obj to clone and an optional cache object to store references to cloned objects and avoid infinite recursion. If the input obj is not an object or is null, the function returns the input obj as-is, since there is nothing to clone. If the input obj is an instance of Date, the function creates a new Date object with the same time value and returns it. This is necessary because Date objects are mutable and should be cloned by value, not reference. If the input obj has already been cloned before (as determined by checking if it exists in the cache object), the function returns the previously cloned object instead of creating a new one. This avoids infinite recursion when cloning circular references. The function creates a new object clonedObj with the same prototype as the input obj using Object.create and sets its property descriptors to match those of the input obj using Object.getOwnPropertyDescriptors. The function then iterates over all properties of the input obj, including non-enumerable properties and properties with symbols as keys, using Object.getOwnPropertyNames and Object.getOwnPropertySymbols. For each property, the function retrieves its descriptor using Object.getOwnPropertyDescriptor. If the property is configurable, the function recursively clones its value using deepClone. The function then sets the property on the clonedObj using Object.defineProperty. Finally, the function stores the obj-clonedObj mapping in the cache object to avoid infinite recursion and returns the cloned object clonedObj.

Why are we using WeakMap here instead of usual javascript objects for storage?

The main difference between the two is that a WeakMap allows for garbage collection of objects that are no longer being used. This means that if the original object or any of its properties are no longer referenced elsewhere in your program, they will be automatically removed from the WeakMap, freeing up memory.

On the other hand, if you use a normal JS object to cache already cloned objects, those objects will remain in memory until the entire object holding them is garbage collected. This can lead to memory leaks if the original object is very large or if the cloning process is repeated many times.

Another advantage of using a WeakMap is that it can be more secure than using a plain JS object. Because a WeakMap only holds weak references to objects, it is less likely to interfere with the garbage collector or cause memory leaks.

Again, it is not necessary to definitely go with WeakMap always, we can choose JS objects as well depending on our use cases.

I hope this article was helpful to you. If yes, please make sure to read my other articles. I am currently writing a series on common "Frontend engineering interview questions" based on my experience interviewing people at Amazon, RedHat, VMware, and Snapdeal, ...

Please subscribe to the newsletter to get the articles delivered directly to your mailbox.

Cheers

Arunkumar Sri Sailapathi.

#2Articles1Week