Making a basic deepCopy function in JS
Tags: javascript, webdev, uiweekly, concepts
Published: 2021-06-14 19:27:16

##Important - If planning to read this article, do it completely as there are some corrections done later. 

Okay let's start 👇

By default in JS, if we try to make a copy of an object, say `obj`, then either of the two helps us create **Shallow** copies :-

- `Object.assign({}, obj)`
- `{...obj}`

And the notorious yet popular `JSON.parse(JSON.stringify(obj))` workaround can help us make a deep copy with the following limitations :-
- If `obj` has **methods**, they won't be copied.
- If `obj` has **circular references**, the above would simply throw an error.

This gives us an opportunity to make our own `deepCopy` function which can deal with the above limitations.

Let's dive into its epic creation via a conversation between **Shalu** and **Deepu**.

**Shalu** - I had a JS interview today and the interviewer asked me to build a custom `deepCopy(obj)` function to do guess what ? DEEP COPYING !!! But I only knew `JSON.parse(JSON.stringify(obj))` workaround which clearly had limitations as pointed by the interviewer. 

**Deepu** - Don't worry. We will try to implement our own basic `deepCopy(obj)` function which also takes care of those limitations. We will start simple, and gradually transform our function for the requirements. Take a look at this function :-

```js
function deepCopy(obj) {
  const newObj = Array.isArray(obj) ? [] : {};
  for (const [key, value] of Object.entries(obj)) {
    newObj[key] = typeof value === 'object' ? deepCopy(value) : value;
  }
  return newObj;
}
``` 

**Shalu** - Well that's not gradual at all....

**Deepu** -  Okay wait....
![let me explain](https://pbs.twimg.com/media/EjuLKFkVcAEgU7p.jpg)

```js 
const newObj = Array.isArray(obj) ? [] : {};
``` 
**Deepu** - We are initializing `newObj` to an empty `Array` or a `POJO` (Plain Old JavaScript Object) on basis of whether `obj` is an array or not. 

```js 
for (const [key, value] of Object.entries(obj)) {
    newObj[key] = typeof value === 'object' ? deepCopy(value) : value;
  }
  return newObj;
```
Suppose `obj` was `{ name:'Saitama', age:'26' }`, then `Object.entries(obj)` would return an array`[ ['name','Saitama'],['age','26'] ]`.

So we are looping over de-structured `key`-`value` pair from this array and performing a conditional check. 

The check is that if type of `value` is `object`, then assign the result of `deepCopy(value)` to `newObj[key]` else just assign `value` itself. 

**Shalu** - Wait a minute !!! We are calling `deepCopy(...)` from within `deepCopy(...)`. Isn't that recursion ?

**Deepu**

![goddamn right](https://i.pinimg.com/originals/4a/cb/83/4acb83efaab972a7bb25ec62a333753f.jpg)

This use-case requires recursion. We don't know how many layers of nested objects our main `obj` might have. We only know that if the corresponding `value` for a `key` is not of type `object`, we can safely put the same `key`-`value` pair in our `newObj`. For the rest, we need to call `deepCopy(value)` again. 

**Shalu** - But wait !!! What about **Functions** ? They are also JS Objects only right ?

**Deepu**
![obama you are right](https://memegenerator.net/img/instances/54099479.jpg)

They indeed are but their `typeof` is `function`. And this particular thing really works for us since we only need to assign these functions as `value` to a particular `key` and not worry about any nesting which is in the case of `{ }` or `[ ]`. 

**Shalu** - So this is it right ?

**Deepu** - Well not quite yet. The above will fail tragically in the case of circular references.

![no failure](https://media2.giphy.com/media/d2ZbrRZxocUzcakM/200.gif)

**Shalu**

![why tell me why](https://66.media.tumblr.com/aaa2ab7bebab006f26602038121e4b6e/tumblr_o5abkii8SS1tfbtrwo1_400.gif)

**Deepu** - Remember how we are recursing whenever the type of `value` is `object` ? Now consider that after 3 depths of recursion, we arrive at a `key` whose `value` is again the main `obj` i.e. there is a circular reference from a nested `key` to the main `obj` itself. This will result in an infinite loop of menace !!

![infinity](https://media4.giphy.com/media/U2BASTIsaw8WQ/giphy.gif)

**Shalu** - Oh damn!!! How would you handle this ?

**Deepu** - Well let's see what do we have at disposal. We need a mechanism to not recurse over already processed or seen object references. 

**Shalu** - Cool so let's make a new obj, say , `const seen = { }` and use it as a dictionary.

**Deepu** - Well we need object references as key and `{ }` only takes **strings** as **keys**.

**Shalu** 
![Pikachu meme face](https://i.kym-cdn.com/entries/icons/original/000/027/475/Screen_Shot_2018-10-25_at_11.02.15_AM.png)

**Deepu** - We can make use of `Map` or `Set` here with the latter making more sense. And to take things up a notch, let's make use of `WeakSet`. 

**Shalu** - Why `WeakSet` ?

**Deepu** - Because [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakSet) says so !!

> Functions that call themselves recursively need a way of guarding against circular data structures by tracking which objects have already been processed. WeakSets are ideal for this purpose.

**Shalu** - Alright I am excited for the final code 

![excited](https://media1.giphy.com/media/pUs87dXYIIrTy/source.gif)

**Deepu**

![here we go](https://i.gifer.com/fxsc.gif)

```js
 function deepCopy(obj) {
  const seen = new WeakSet();

  function logic(obj) {
    const newObj = Array.isArray(obj) ? [] : {};
    if (!seen.has(obj)) {
      seen.add(obj);
      for (const [key, value] of Object.entries(obj)) {
        newObj[key] = typeof value === 'object' ? logic(value) : value;
      }
    } else {
      return obj;
    }
    return newObj;
  }
  
  return logic(obj);
} 
```

**Shalu** - Damn that's quite big now. 

**Deepu** - Well the flow is still simple. What we now did is initialize a `WeakSet` by the name `seen` inside `deepCopy(...)`. And since we always needed access to `seen` while recursing, we extract all our recursion logic inside this `logic(...)` function. Also note we have applied the check using `seen` for the `obj` reference and if it doesn't exist, we add it to `seen`. Else, we don't bother performing the **for loop** logic for it and return the `obj` as it is. At the end of `deepCopy(...)` function we call `logic(obj)` (which will internally recurse as needed) as well as return its result.

**Shalu** 
![wow](https://lifesfinewhine.com/wp-content/uploads/2018/10/graphic-post.gif)


**Thank you everyone who read it till here. This is an implementation that I have tried without referring anything online with the mindset that how will I do this if asked in an interview. Obviously the flow will be the same minus the incredible gifs 😉 and you are free to evaluate me as an interviewer**.

### Correction
I got an important feedback from the comments that the above implementation doesn't clone the circular reference cycle successfully because I am returning the original `obj` when it's already present in `seen`. I should have been returning `newObj` corresponding to that `obj` here. For that, we would get rid of `WeakSet` altogether and use `WeakMap` instead like so :-

```js
 function deepCopy(obj) {
  const seen = new WeakMap();

  function logic(obj) {
    const newObj = Array.isArray(obj) ? [] : {};
    if (!seen.has(obj)) {
      seen.set(obj, newObj);
      for (const [key, value] of Object.entries(obj)) {
        newObj[key] = typeof value === 'object' ? logic(value) : value;
      }
    } else {
      return seen.get(obj);
    }
    return newObj;
  }

  return logic(obj);
} 
```
### Possible enhancement - 1
```js
 function deepCopy(obj) {
  const seen = new WeakMap();

  function logic(obj) {
    // Creating dynamic newObj using constructor
    const newObj = new obj.constructor();
    if (!seen.has(obj)) {
      seen.set(obj, newObj);
      for (const [key, value] of Object.entries(obj)) {
        newObj[key] = typeof value === 'object' ? logic(value) : value;
      }
    } else {
      return seen.get(obj);
    }
    return newObj;
  }

  return logic(obj);
} 
```

### BONUS - Fancy Reduce edit

```js
function deepCopy(obj) {
  const seen = new WeakMap();

  function logic(obj) {
    if (!seen.has(obj)) {
      return Object.entries(obj).reduce((newObj, [key, value]) => {
        seen.set(obj, newObj);
        newObj[key] = typeof value === 'object' ? logic(value) : value;
        return newObj;
      }, new obj.constructor())
    } else {
      return seen.get(obj);
    }
  }

  return logic(obj);
}
```