There are two types of clones in JavaScript: deep, and shallow. To fully understand the difference, we need to dive deeper into how the interpreter treats different types of variables. Let’s go!
Primitive types
null
undefined
boolean
number
string
symbol
These are the types that are immutable. Some of you might be surprised – we can, in fact, extend a string for example. Let me use some good old string concatenation to better illustrate what I have in mind.
1 2 |
let fullName = 'John'; fullName += 'Smith'; |
What happens here is that in the second line, we assign a brand new string to the variable named fullName, even if it might seem like modifying an existing one. Same goes for other primitive types. That is the reason why I used let instead of const here.
Using the latter would result in:
Uncaught TypeError: Assignment to constant variable.
Another thing to keep in mind is that primitive types can’t have any methods or properties. But can they not, really?
1 2 |
console.log(fullName.length); // 13 console.log(fullName.split(' ')); // ["John", "Smith"] |
It is a valid piece of code thanks to the fact that almost all primitive types (except for null and undefined) have an object that wraps around the primitive value. Let me do a little bit of trickery to prove it.
1 2 3 4 5 6 7 |
console.log(typeof fullName); // 'string' String.prototype.getType = function () { return typeof this; }; console.log(fullName.getType()); // 'object' |
The wrapper for that particular variable of primitive type is not kept for long though: as soon as the work is done, it is gone.
A way to observe it is to try to define a property for the string and assign a value to it.
1 2 |
fullName.lastName = 'Smith'; // no error here console.log(fullName.lastName); // undefined |
Fire up the console and experiment a little.
Reference type
The only reference type is an object – it can have properties and methods. Arrays are also objects! They are just an instance of an Array and inherit from Array.prototype. Beware of some rookie mistakes!
1 2 3 4 5 |
if(typeof someVariable === 'object'){ console.log('It is an object!); } else if (someVariable instanceof Array){ console.log('This block of code will never run because of the else statement!'); } |
The crucial concept to grasp in the context of cloning objects is how the references are handled.
1 2 3 4 5 6 7 8 9 10 11 |
const userTemplate = { firstName: 'John', lastName: 'Smith', } const john = userTemplate; const dorothy = userTemplate; dorothy.firstName = 'Dorothy'; console.log(john.firstName); // 'Dorothy' |
As you can see, changing properties of Dorothy caused them to change also for John – this is because they are both a reference
to the same object!
Shallow clone
Situation like the one described above often creates a need to make a clone. The most basic one you can do is called a shallow clone.
There are a few options here. My personal favourite includes using a spread operator.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
const userTemplate = { firstName: 'John', lastName: 'Smith', city: { name: 'New York', country: 'United States' } } const john = { ...userTemplate }; const dorothy = { ...userTemplate }; dorothy.firstName = 'Dorothy'; dorothy.city.name = 'Boston'; console.log(john.firstName); // 'John' console.log(john.city.name); // 'Boston' |
As you can see, changing the firstName property for Dorothy no longer messes with John. A clone like that is called shallow
for a reason, though – as you can see, it only cloned the first layer of the object – that means that all of the clones share the same instance of the city property.
There is a way to prevent mutating our template a little using Object.seal and Object.freeze.
The first one will prevent adding and removing any properties from the object (throwing an error), while the latter will also prevent modifying the existing properties. Keep in mind that it will result in an error if you are in strict mode. If you’re not, it will still prevent the mutation, though.
Unfortunately, it is shallow and won’t prevent mutating any object properties – it means that in our userTemplate, we will still be able to change properties of the city, even if its parent is frozen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
'use strict' const userTemplate = Object.freeze({ firstName: 'John', lastName: 'Smith', city: { name: 'New York', country: 'United States' } }) userTemplate.firstName = 'Dorothy'; // Uncaught TypeError: Cannot assign to read only property 'firstName' of object delete userTemplate.firstName; // Uncaught TypeError: Cannot delete property 'firstName' userTemplate.preferedLanguage = 'English'; // Uncaught TypeError: Cannot add property preferedLanguage, object is not extensible userTemplate.city.name = 'Boston'; // No error here! |
Cloning with a spread operator creates a new object with all of the properties of the old one (methods included). It does not copy the chain of Prototypes though, so watch out!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Fruit { constructor(type, color){ this.type = type; this.color = color; } getColor(){ return this.color; } } const redApple = new Fruit('apple', 'red'); const greenApple = { ...redApple }; greenApple.color = 'green'; console.log(greenApple); // { type: 'apple', color: 'green' } console.log(greenApple.getColor()); // Uncaught TypeError: greenApple.getColor is not a function |
A very similar way to create a clone is to use Object.assign()
1 |
const greenApple = Object.assign({}, redApple); |
This function copies all of the properties of the old object straight to the new object and returns it. It won’t copy the chain of Prototypes either.
Just keep in mind that both of these solutions require ES6. Making a shallow copy without it will require writing a polyfill, or using one of many libraries such as lodash that have this functionality.
Deep clone
This is where things get a little trickier. The most basic way to create a deep clone is to use a built-in JSON parser.
1 |
const deepClone = JSON.parse(JSON.stringify(oldObject)); |
There are a lot of drawbacks here, though. As you may have guessed already, it won’t copy the chain of Prototypes either. To make things worse, it won’t even copy any methods along the way – they are not included in the JSON format after all – this might give you quite a headache.
If that’s fine by you, that’s great.
If not, you will once again depend on external libraries or will need to write additional logic yourself. In another article, I will walk you through writing such function yourself. We will also publish it on Node Package Manager (NPM) registry, so stay tuned!