JavaScript Deep Cloning, and Value vs Reference

Evan Winston
Irrelevant Code
Published in
6 min readMar 18, 2019

If you’ve spent any time learning the quirks of JavaScript, or ever been caught unawares by side effects when trying to copy arrays or objects, you’ve likely run across conversations comparing value to reference in JavaScript. It can be a tricky topic.

Value vs Reference

In nutshell, JavaScript’s primitive data types are passed by value — booleans, strings, numbers, null, and undefined. When these values are contained in a variable or property, they are exactly that, contained within. The value is immediate, literal, and non-referential. Let’s take a look at it in code:

var x = 7;
var y = x;
console.log(x); // 7
console.log(y); // 7
x = x + 3;console.log(x); // 10
console.log(y); //7

In the example above, variable x is assigned the primitive value of 7. x contains the value of 7. Then variable y is assigned the value of x (which, remember, contains the value of 7). y now also contains the value of 7. y does NOT contain a reference to the value contained in x!

Then, the value of x is increased by 3. x then contains a value of 10, but y STILL contains a value of 7 (again, because the value assigned to y was a primitive itself, and not a reference to x).

TLDR: x contains a primitive value, so by assigning x as a value to y, we effectively made a copy of its value. Afterwards, changing the value of x does not affect the value of y.

If that’s still confusing, hold tight and let’s compare it to the counterpart: reference.

In addition to primitives, 3 JavaScript data types are not passed by value, but by reference — arrays, objects, and functions.

var a = { color: 'red' };
var b = a;
console.log(a); // { color: 'red' }
console.log(b); // { color: 'red' }
b.color = 'blue';console.log(a); // { color: 'blue' }
console.log(b); // { color: 'blue' }

As you can see, in assigning the value of variable a to variable b, it at first looks like we’ve made a copy, in the same way we did when dealing with a primitve. However, when we change the color property in one of the two variables to ‘blue’, we can see that the color property of BOTH variables reflects the change.

This is because, when a is assigned to b, variable b doesn’t actually contain a separate, distinct object with a color property of red; it contains a reference to the object in variable a — a pointer to the same piece of data, rather than a copy of that data.

The Problem of Cloning

There comes a time, when writing JavaScript, where one has to copy a non-primitive piece of data, such as an array or an object, usually in order to non-destructively manipulate it. This is easier said than done, for the reason shown above.

That said, there are solutions readily available. The danger of these solutions comes from taking for granted how they are working behind the scenes. I’ve seen plenty of students turn to the Internets for a copy-and-paste answer, and be surprised by unintended side effects from untested edge cases.

In other words, the only way to reliably source a solution to this problem is by being aware of the distinction between a shallow copy, or shallow cloning, and a deep copy, or deep cloning.

To illustrate, let’s take a look at some of the ready-made solutions out there.

Shallow Cloning

Shallow cloning is effectively copying an array or object and cloning any values contained therein; HOWEVER, any references contained in these data types will not be copied, and will remain as references. To put it another way, any values contained at the top level of hierarchy in an object or array will be effectively copied, but any data at subordinate levels of hierarchy will not be.

As a result, shallow cloning methods can be deceptive, often misleading developers into thinking they have accomplished a full deep clone, and exposing their code to lurking side effects.

Let’s demonstrate, using the most out-of-the-box method for performing a shallow clone, Object.assign():

const myObject = {
name: 'Evan Winston',
age: 30,
gender: 'male',
contact: {
home: '222-222-2222',
mobile: '333-333-3333',
email: 'evanw89@gmail.com',
address: '123 Main St, San Francisco, CA 94106'
}
};
const copiedObject = Object.assign({}, myObject);console.log(copiedObject.name); // 'Evan Winston'
console.log(copiedObject.contact.email); // 'evanw89@gmail.com'
myObject.name = 'Gary Winston';console.log(myObject.name); // 'Gary Winston'
console.log(copiedObject.name); // 'Evan Winston'

Up to this point, everything looks good. The values in our copied object appear to exist independently of the original values in myObject (changing the name property in myObject does NOT change the name property in copiedObject; that’s what we want!). But this breaks down when we test the same principle on the contact property, which doesn’t contain a data primitive, but an object itself (a subsequent level of data hierarchy) which remains a reference rather than a distinct copy:

myObject.contact.email = 'evan@yahoo.com';console.log(copiedObject.contact.email); // 'evan@yahoo.com'

That’s a big no-no, and one we might not have seen if (1) our original data did not contain any non-primitive reference data types, in which case all of our data would have been copied successfully and we may have made an assumption about the Object.assign() method that isn’t completely true, or (2) we had not tested the references in our original data to determine that they had not been successfully cloned in our copied data.

An identical such solution is using the Object Spread Operator:

const copiedObject = {...myObject };

This will achieve identical results to the Object.assign() example above.

Another misleading and highly popular solution is using JSON Serialization, or first transforming the original data into JSON format and parsing the result to produce a copy. Much like Object.assign(), this appears to work very well until you dig a little deeper.

This will not work on any JavaScript data which doesn’t have an equivalent in JSON, the most obvious being functions!

const originalObj = {
a: new Date(),
b: NaN,
c: new Function(),
d: function(){},
e: Infinity
}
const copy = JSON.parse(JSON.stringify(originalObj));console.log(copy);
// {
a: "2019-03-18T05:07:51.283Z",
b: null,
e: null
}

As you can see, any property without a JSON equivalent is either completely disregarded or mutated to an inappropriate null.

Finally, a solution I see used frequently is using Object.create(). I’m not sure how this has propagated, as it doesn’t even create a shallow clone, it simply creates a new object with the original object being used as a prototype of the new object.

const obj = {
name: 'Evan'
}
const newObj = Object.create(obj);console.log(newObj.name); // 'Evan'

At first glance, it seems to have worked, but only at first glance:

console.log(newObj); // {}obj.name = 'Matt';console.log(newObj.name); // 'Matt'obj.hasOwnProperty('name'); // true
newObj.hasOwnProperty('name'); // true

I would 100% never recommend using Object.create() to clone.

Deep Cloning, The Solution

So what’s the proper solution? Lodash offers an easy, accessible solution by exposing the library’s deepclone function, but I’d never recommend using a library when vanilla JS will do:

function deepCopy(object) {
var output = Array.isArray(object) ? [] : {};
for(var data in object) {
var value = object[data]
output[data] = (typeof value === "object") ? deepCopy(value) : value;
}
return output;
}

This may be intimidating if you’re not comfortable interpreting ternary expressions, but it’s a fairly straightforward solution, which even utilizes recursion to ensure it will successfully deep clone as many levels of data hierarchy as are present in the original data.

const myObject = {
name: 'Evan Winston',
age: 30,
gender: 'male',
contact: {
home: '222-222-2222',
mobile: '333-333-3333',
email: 'evanw89@gmail.com',
address: '123 Main St, San Francisco, CA 94106'
}
};
const copiedObject = deepCopy(myObject);myObject.contact.email = 'evan@aol.com';console.log(copiedObject.contact.email); // 'evanw89@gmail.com'

And there you have it! A successful deep clone which will work on arrays or objects, no matter how deeply nested. Try it out!

Evan is an illustrator, developer, designer, and animator who tells stories in any which way he can. When he’s not branding businesses or building front-end apps; he’s illustrating children’s books, painting for tabletop games, animating commercials, or developing passion projects of his own.

--

--