DEV Community

Amit Khonde
Amit Khonde

Posted on • Edited on • Originally published at amitkhonde.com

JavaScript Interviews: Create a deep copy of an object

What is this series about?

Hello all! Welcome to the JavaScript interview questions series. In each post of this series, I will talk about the questions (specific to JavaScript) I faced in my recent interviews. This series will be helpful for you if you are preparing for JavaScript interviews or just started to deep dive into JavaScript and want to test your knowledge.

Post 1: Create a deep copy of an object

Many of us who have worked on any fairly large side projects or have contributed to other side projects must have come across JavaScript helper libraries like Lodash.js, Underscore.js. These libraries provide us with helper functions for things that JavaScript does not provide built-in. One of those functions is copying objects in JavaScript. A lot of us know how to copy objects which only have one level of nesting by Object Destructing. But if your object contains multiple nested levels, there is no in-built way in JavaScript to copy that object.

A lot of you might be wondering why this question is asked? If we have the helper library, why not just use that? And you are absolutely right. We should use that and we do use them indeed. But writing such a core function is going to test how you grasp and apply things fundamentally. As we will see later in this article, this question tests how you apply the knowledge that you already have. So let us get into some problem-solving mode πŸ‘¨β€πŸ’»βš”οΈ.

Problem Statement

Write a function that will take an object as an argument and returns a deep copy of that object.

// Signature
function copyObject(source) {

}

// Usage
const source = {
    a: 10,
    b: 20,
    c: {
        d: 30
    }
}

const target = copyObject(source);
Enter fullscreen mode Exit fullscreen mode

Before diving into the solution, I highly suggest that you try to solve this problem on your own. Here are some hints:

  • Forget about the nesting part. First, just try to copy each key and value.
  • Now think about how you can identify if a value is an object itself and what to do with it.

Solution

When I am solving any problem, I always like to write the obvious things first. Those things can be found by reading the problem statement carefully. The very obvious thing that the question asks is to return an object. So let us write that down first.

function copyObject(source) {
    var target = {};

    return target;
}
Enter fullscreen mode Exit fullscreen mode

Now, the problem asks us for a deep copy of the object. But before directly jumping to deep copy, let us first write a simple solution for copying each key value for a single level of nesting. So what do we need for that?

  • We need all the keys from the source object
  • Add all those keys one by one in the target object.
function copyObject(source) {
    var target = {};
    const keys = Object.keys(source);
    keys.forEach(key => {
        target[key] = source[key];
    });

    return target;
}
Enter fullscreen mode Exit fullscreen mode

Great! So we have solved the problem for the simplest use case. Now let us think about nesting. So first of all, how will we know if the value corresponding to the current key is an object itself? By using typeof operator. And when we know that the current value is an object, how can we get its copy? --> By using the function that we are writing. I know this might sound confusing right now. This technique is known as Recursion (You can learn more about recursion here). Let us just write the code and you will understand. So the final solution to the problem will look like this:

function copyObject(source) {
    var target = {};

    // Getting source object keys
    const keys = Object.keys(source);
    keys.forEach(key => {
        // Checking if current value is an object
        if (typeof source[key] === "object") {
            // Calling our function recursively for current value
            target[key] = copyObject(source[key]);
        } else {
            // Directly assigning the value
            target[key] = source[key];
        }
    });

    return target;
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Yay!! This looks like a working solution now. But there are still minor problems with this solution like handling array and function values in objects. I would encourage you to write the code that will handle these conditions and post it in the comments. And for more interesting questions like this, keep following this series. Until then, Happy Coding!!

Top comments (24)

Collapse
 
garystorey profile image
Gary Storey • Edited

You can also use
JSON.parse(JSON.stringify(obj))

Collapse
 
jankapunkt profile image
Jan KΓΌster

But stringify removes lots of things so you need to write reviver etc. to support instances or constructor refrrences

Collapse
 
garystorey profile image
Gary Storey

This example, and the problem statement, are using a nested data structure; not functions or constructors. For the purposes outlined above this is perfectly valid.

Collapse
 
thesanjeevsharma profile image
Sanjeev Sharma

Haha! Gary 1 Interviewer 0 πŸ˜‚

Collapse
 
sgoulas profile image
sgoulas

This is indeed a nice trick, but do keep in mind that in order for this to work the object must not contain dates, functions, undefined, infinity, nan, regexes, maps, sets, blobs, file lists, image data, sparce / typed arrays or any other complex types. For the majority of the cases though it will do the trick.

Collapse
 
garystorey profile image
Gary Storey

If there had been any of the above mentioned, then I would not have recommended my approach.

As you stated this works in the majority of situations and for the examples given.

Collapse
 
jankapunkt profile image
Jan KΓΌster • Edited

Some input for a follow-up article or for those who like to get challenged:

const source = {
  type: Uint8Array,
  optional: false,
  createdAt: new Date(),
  references: [source],
  message: new TextEncoder().encode('Hello World')
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
zer0 profile image
zer0 • Edited

Wouldn’t the spread operator work here too ?

const clone = {...original};
Enter fullscreen mode Exit fullscreen mode

I know it technically only clones the top keys but the disconnecting effect of not having a reference to the original is given

Collapse
 
taufik_nurrohman profile image
Taufik Nurrohman

Liquid syntax error: 'raw' tag was never closed

Collapse
 
zer0 profile image
zer0 • Edited

honestly thats not really 2021.
thats the old version of the ... spread operator

Collapse
 
taufik_nurrohman profile image
Taufik Nurrohman

LOL!

Collapse
 
jackhpeterson profile image
Jack H. Peterson

This clones, but doesn't deep clone. If you mutate an object nested within source it'll also mutate the target and vice versa.

Collapse
 
taufik_nurrohman profile image
Taufik Nurrohman • Edited

it'll also mutate the target

That will not happen because the first argument is a new object.

To compare:

// :)
Object.assign({}, source);

// :(
Object.assign(target, source);
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
carnicero90 profile image
Filippo Montani

You can pretty straightforwardly check if that's the case by testing it in the console.

Thread Thread
 
jackhpeterson profile image
Jack H. Peterson • Edited

Objects are passed by reference, not value.

When you clone (assign) an object, it doesn't clone the values of objects within, it clones the reference. Which means both objects, the original and the clone are referencing the same nested objects.

It's a tricky and sometimes frustrating quality of JS. Strings, numbers, bools will clone as expected, but objects will continue to reference the same thing.

Think of assigning a DOM element to a variable. The DOM element still exists, and modifying the variable will still affect the DOM. That's because objects are passed by reference!

Thread Thread
 
taufik_nurrohman profile image
Taufik Nurrohman

The DOM part makes me get it.

Collapse
 
alohci profile image
Nicholas Stimpson • Edited
let a = {};
let b = { foo: a };
a.bar = b;
let c = copyObject(b);
Enter fullscreen mode Exit fullscreen mode

Watch out for circular references in your recursion.

Collapse
 
darkwiiplayer profile image
π’ŽWii πŸ³οΈβ€βš§οΈ • Edited

Preventing endless recursions in the case of circular references can be a bit trickier, specially if you want to reconstruct those same circular structures in the copy of the object. Definitely a good follow-up question, but not something I'd normally expect to be answered flawlessly during an interview, more of a "how'd you go about this?" sort of question.

EDIT: Definitely something I'd expect someone to notice, either in the process of coming up with a simple solution (aka. asking whether loops need to be considered), or at least when asked to find possible flaws in their solution.

Collapse
 
sprakashk profile image
Satyaprakash Kumawat

Object.prototype.toString.call(source[key]) === "[object Object]" can be used to identify if the value is an Object and rest can be directly put in the resulting object.

if the source input is as below:

let source = {
a: 10,
b: 20,
c: [1,2,3,4]
}

the output will be. here the c was n array and it got converted to object.

{
"a": 10,
"b": 20,
"c": {
"0": 1,
"1": 2,
"2": 3,
"3": 4
}
}

If we use Object.prototype.toString.call(source[key]) === "[object Object]" it will clone properly.

Collapse
 
cullophid profile image
Andreas MΓΈller

typeof null === "object", so you have to add a special clause for that :)

Collapse
 
pris_stratton profile image
pris stratton

My first thought was wouldn’t I just use Object.assign, is that a valid answer?

Collapse
 
ninofiliu profile image
Nino Filiu

Object.assign is equivalent to object spread and only copies the top level keys of an object

const a = { b: { c: 10 } }
const clonedA = { ...a }
clonedA.b.c++
a.b.c // 11
Enter fullscreen mode Exit fullscreen mode
Collapse
 
hakki profile image
Hakki
Collapse
 
dvddpl profile image
Davide de Paolis

Not in the provided sample, but if you are deep cloning you might want to consider if one of the values is an array and also if the values contained in the array are object themselves