This content originally appeared on DEV Community and was authored by Lakshya Thakur
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;
}
Shalu - Well that's not gradual at all....
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.
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
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 ?
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.
Shalu
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 !!
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.
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
Deepu
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.
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.
BONUS - Fancy Reduce edit
function deepCopy(obj) {
const seen = new WeakSet();
function logic(obj) {
if (!seen.has(obj)) {
seen.add(obj)
return Object.entries(obj).reduce((newObj, [key, value]) => {
newObj[key] = typeof value === 'object' ? logic(value) : value;
return newObj;
}, Array.isArray(obj) ? [] : {})
} else {
return obj;
}
}
return logic(obj);
}
This content originally appeared on DEV Community and was authored by Lakshya Thakur
Lakshya Thakur | Sciencx (2021-06-14T19:27:16+00:00) Making a basic deepCopy function in JS. Retrieved from https://www.scien.cx/2021/06/14/making-a-basic-deepcopy-function-in-js/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.