DEV Community

loading...
Cover image for Making a basic deepCopy function in JS

Making a basic deepCopy function in JS

Lakshya Thakur
I like to have creative insights towards my subjects and apply my engineering skills for problem solving.
・5 min read

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 :-

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;
}
Enter fullscreen mode Exit fullscreen mode

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

Deepu - Okay wait....
let me explain

const newObj = Array.isArray(obj) ? [] : {};
Enter fullscreen mode Exit fullscreen mode

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.

for (const [key, value] of Object.entries(obj)) {
    newObj[key] = typeof value === 'object' ? deepCopy(value) : value;
  }
  return newObj;
Enter fullscreen mode Exit fullscreen mode

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

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

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

Shalu

why tell me why

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

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

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

Deepu

here we go

 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);
} 
Enter fullscreen mode Exit fullscreen mode

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

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 :-

 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);
} 
Enter fullscreen mode Exit fullscreen mode

Possible enhancement - 1

 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);
} 
Enter fullscreen mode Exit fullscreen mode

BONUS - Fancy Reduce edit

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);
}
Enter fullscreen mode Exit fullscreen mode

Discussion (7)

Collapse
govvj profile image
Govind Vijay

Your logic has a bug.
let a = {}; a.b = {}; a.b.c = a; // circular dependency
a.x = 'x'; // to track if deepclone is working or not
let a_cl = deepCopy(a);
a_cl.x = 'xx';
console.log(a_cl.b.c.x)
Above line print 'x' which is wrong. It should print 'xx';
This value is coming from "a" instead it should be coming from "a_cl".

When you do "return obj;" it does not clone the cycle and istead uses the original cycle.

Collapse
lapstjup profile image
Lakshya Thakur Author

Thank you for pointing that out 😁. I am thinking of fixing it by using WeakMap to store corresponding newObj for obj and returning the newObj when same obj occurs. I think that should work.

Collapse
ky1e_s profile image
Kyle Stephens

It's not pretty, but it works:

const deepCopy = JSON.parse(JSON.stringify(objectToCopy));
Enter fullscreen mode Exit fullscreen mode
Collapse
lapstjup profile image
Lakshya Thakur Author

you don't say

Collapse
ky1e_s profile image
Kyle Stephens

?

Thread Thread
lapstjup profile image
Lakshya Thakur Author

It’s a joke man 😅. I know parse, stringify works. It’s fast too. Actually, one of my friends got this asked in an interview and thus this article.

Collapse
jsnanigans profile image
Brendan Mullins

I would prefer this all day to that chunky angorithm, is there any reason this is worse?