If you have been programming in Javascript for some time now, you have likely worked with copies even if you might not have realized it. The concept of copying might not seem important until you change a value in the ‘copy object’ and expect the ‘original object’ to remain the same. To effectively work with copies in your projects, Javascript includes support for shallow and deep copies.
A shallow copy means some (if not all) of the copied values are still connected to the original. Any operation carried on the ‘copied’ version might affect the original. On the other hand, a deep copy means all copied values are disconnected from the original. Any operation carried on the ‘copied’ version will not in any way not affect the original.
You have probably heard of the principle in functional programming philosophy – “you should not modify the existing data.” To achieve that, you need to know how to safely copy data while avoiding pitfalls. To get started, this post will first give you a detailed refresher on Javascript data types since they have a huge influence on how you copy values.
Table of Contents
Javascript Data Types
There are two main data types in Javascript. Primitive data types and Non-primitive data types. The main difference between the two is how the values are stored in memory. Primitive values are stored by value while non-primitive values are stored by reference.
Primitive Data Types
Below is a list of all primitive types in Javascript.
- String: Store alphanumeric characters and symbols
- Boolean: Hold a ‘true’ or ‘false’ value.
- Number: Hold numbers and decimals.
- Undefined: This means a variable has been created but hasn’t been assigned a value.
- Null: It’s explicitly set by a developer to mean ’empty’ or ‘nothing.’
- Symbol
Non-primitive Data Types
There is only one non-primitive data type in Javascript – ‘Object.’ However, arrays also fall into this category as they are regarded as Objects in Javascript.
Copying Primitive Values
When working with primitive values in Javascript, copying is not an issue since these values exist only once and refer to a single value in memory. Primitive values are tightly coupled with the variable they are assigned to. Therefore, if you make a copy – it is a deep (real) copy. Look at the examples below.
let log = console.log;
let a = 5;
let b = a;
b = 8 //Re-assign the value of 'b'
log(a)
log(b)
/*Output:
5
8
The line let a = 5;
creates a variable a
and allocates space in memory to hold its value. In this case, the value is 5. The line let b = a;
creates another variable b
and allocates its space in memory to store its value. The value, in this case, is ‘the value stored by variable a‘ which is 5.
Note: The variable b
is assigned the value of b
, not the address of a
in memory.
When you reassign the value of b
in the line let b = 8;
, only the value of b
is changed. The value of a
remains the same.
From the examples discussed in this section, it’s clear that when working with primitive values, any copy operation will result in a deep copy. The concept of shallow and deep copies mainly applies when dealing with objects.
Copying Non-Primitive Values
When working with primitives, you learned in the previous section that primitive values are tightly coupled with their assigned variables. Objects, on the other hand, are stored by stored/ held by reference. This “by reference” is what mainly brings up the issue of the shallow and deep copy.
Shallow Copy
Take a look at the code below.
let log = console.log;
//Create the first object, studentOne
const studentOne = {
firstName:"Jane",
lastName:"Doe",
age:25
}
//Create a copy of studentOne object
let studentTwo = studentOne
//Reassign the value of firstName in studentTwo object
studentTwo.firstName="John"
log(studentOne)
log(studentTwo)
/*Output:
{ firstName: 'John', lastName: 'Doe', age: 25 }
{ firstName: 'John', lastName: 'Doe', age: 25 }
The code snippet above is quite simple. You first created a studentOne
object with several key-value pairs. Next, you made a copy of the studentOne
object using the assignment operator (=) to create a second object called studentTwo
. Lastly, you reassigned the value of firstName
in the studentTwo
object.
When you log the two objects, you will notice that the value of firstName
changed both in the ‘copy’ and the ‘original’ object. That’s because when you created a copy of the studentOne
object in the line let studentTwo = studentOne
, you only assigned studentTwo
the address of studentOne
.
In short, both studentOne
and studentTwo
are pointing to the same address in the memory. This is a perfect illustration of a shallow copy. Although we created a new object (studentTwo), its values are still connected to the original object (studentOne).
Deep Copy
There are different ways that you can use to perform a deep copy in Javascript. However, you will notice that these methods work differently with nested and un-nested objects.
Un-nested Objects
When working with non-nested objects (objects that don’t have other objects as values), you can use the following methods to perform a deep copy.
- The spread operator (…)
- The Object.assign() method
Take a look at the code snippets below.
The Spread Operator (…)
The code below illustrates how to carry out a deep copy with the spread operator.
let log = console.log;
//Create the first object
const studentOne = {
firstName:"Jane",
age:25,
}
//Make a copy of studentOne object using the SPREAD operator
const studentTwo = {...studentOne}
//Re-assign the value of firstName in studentTwo object
studentTwo.firstName="John"
log(studentOne)
log(studentTwo)
/*Output:
{ firstName: 'Jane', age: 25 }
{ firstName: 'John', age: 25 }
Here, you can see that the value of firstName
in the original object did not change even after reassigning it using the copied object.
The Object.assign() method
The code below illustrates how to carry out a deep copy with the Object.assign()
method.
let log = console.log;
//Create the first object
const studentOne = {
firstName:"Jane",
age:25,
}
//Make a copy of studentOne object using the Object.assign() method
const studentTwo = Object.assign({},studentOne)
//Re-assign the value of firstName in studentTwo object
studentTwo.firstName="John"
log(studentOne)
log(studentTwo)
/*Output:
{ firstName: 'Jane', age: 25 }
{ firstName: 'John', age: 25 }
Similar to using the spread operator, only the value of firstName in the copied object change. The original object was not affected.
Up to this point, you have learned how to create deep copies when working with un-nested objects. In the next section, you will learn how to work with nested objects.
Nested Objects
Tip: The spread
operator and the Object.assign()
method will only do a partial copy when used with nested objects.
Partial Deep Copy
Take a look at the object below.
const studentOne = {
firstName:"Jane",
age:25,
skill:{
primary:"Web developer",
secondary:"DevOps"
}
}
Make a copy using the spread operator.
//Make a copy of studentOne object using the SPREAD operator
const studentTwo = {...studentOne}
//Re-assign the value of firstName in studentTwo object
studentTwo.firstName="John"
studentTwo.skill.primary="cybersecurity"
log(studentOne)
log(studentTwo)
/*Output:
{
firstName: 'Jane',
age: 25,
skill: { primary: 'cybersecurity', secondary: 'DevOps' }
}
{
firstName: 'John',
age: 25,
skill: { primary: 'cybersecurity', secondary: 'DevOps' }
}
Make a copy using the Object.assign() method.
//Make a copy of studentOne object using the Object.assign() operator
const studentTwo = Object.assign({},studentOne)
//Re-assign the value of firstName in studentTwo object
studentTwo.firstName="John"
studentTwo.skill.primary="cybersecurity"
log(studentOne)
log(studentTwo)
/*Output:
{
firstName: 'Jane',
age: 25,
skill: { primary: 'cybersecurity', secondary: 'DevOps' }
}
{
firstName: 'John',
age: 25,
skill: { primary: 'cybersecurity', secondary: 'DevOps' }
}
When you look at the output of the two code snippets above, you will notice that after reassigning the values firstName
and skill.primary
using the copied object, the value of skill.primary
changed in both the ‘copied’ object and the ‘original’ object. The firstName
value only changed in the ‘copied’ object.
That’s because the spread operator and the Object.assign()
method only did a partial deep copy. The values firstName
and age
were deeply copied while the nested object skill
was shallowly copied.
So, what’s the best solution for the partial deep copy problem?
JSON.Parse(JSON.stringify()) Method
The JSON.parse(JSON.stringify(object)) methods are used to solve the issue of partial deep copy that arise with the spread operator and the Object.stringify() method. Together, these methods first stringify the object to a JSON string and then parse it to an object.
Take a look at the code below.
let log = console.log;
//Create the first object
const studentOne = {
firstName:"Jane",
age:25,
skill:{
primary:"Web developer",
secondary:"DevOps"
}
}
//Make a copy of studentOne object using the JSON methods
const studentTwo = JSON.parse(JSON.stringify(studentOne))
//Re-assign the value of firstName in studentTwo object
studentTwo.firstName="John"
studentTwo.skill.primary="cybersecurity"
log(studentOne)
log(studentTwo)
/*Output:
{
firstName: 'Jane',
age: 25,
skill: { primary: 'Web developer', secondary: 'DevOps' }
}
{
firstName: 'John',
age: 25,
skill: { primary: 'cybersecurity', secondary: 'DevOps' }
}
In the output above, you can see that after the reassignment, only the ‘copied’ object values changed. The ‘original’ object was not affected. That is a perfect illustration of a deep copy in Javascript. The copied values are not in any way connected to the original values.
However, there is a catch! Take a keen look at the code below.
let log = console.log;
//Create the first object
const originalTask = {
name:"Learn React",
duration: function(params){
return "24hrs"
},
date:new Date(),
}
//Make a copy of studentOne object using the SPREAD operator
const copiedTask = JSON.parse(JSON.stringify(originalTask))
log("Original Date = "+typeof(originalTask.date))
log("Copied Date = "+typeof(copiedTask.date))
log(originalTask)
log(copiedTask)
/*Output:
Original Date = object
Copied Date = string
{
name: 'Learn React',
duration: [Function: duration],
date: 2022-09-12T08:07:01.163Z
}
{ name: 'Learn React', date: '2022-09-12T08:07:01.163Z' }
In the code above, you can see the originalTask
object which includes two methods as its values. However, there are two interesting things that happen when you make a copy of the originalTask
object using the JSON.parse – JSON.stringify methods.
- The function in the ‘duration’ key inside
originalTasks
is missing/ lost in thecopiedTask
. - Using the ‘typeof’ operator, you will notice that the ‘date’ in the
originalTask
is an object but the ‘date’ in thecopiedTask
is a string.
Although you have made a deep copy of the originalTask
object, the resulting object (copiedTask
) is lacking several key features which might result in errors in your application.
So, what’s the solution?
Using Lodash to Perform a Deep Copy
Lodash is a popular Javascript library that comes with various methods that you can utilize in your program. One such method is the cloneDeep()
function which you can use to deeply clone/copy an object including non-serializable properties that were not supported by the JSON.parse – JSON.stringify methods.
Use the command below to install the Lodash library.
npm install lodash
The code below shows you how to perform a deep copy using Lodash.
const _ = require("lodash")
let log = console.log;
//Create the first object
const originalTask = {
name:"Learn React",
duration: function(params){
return "24hrs"
},
date:new Date(),
}
//Make a copy of studentOne object using the Lodash cloneDeep() method
const copiedTask = _.cloneDeep(originalTask)
log("Original Date = "+typeof(originalTask.date))
log("Copied Date = "+typeof(copiedTask.date))
log(originalTask)
log(copiedTask)
/*Output:
Original Date = object
Copied Date = object
{
name: 'Learn React',
duration: [Function: duration],
date: 2022-09-12T09:14:04.703Z
}
{
name: 'Learn React',
duration: [Function: duration],
date: 2022-09-12T09:14:04.703Z
}
From the output above, you can see all the issues that were occurring with the JSON.parse – JSON.stringify methods are solved. The ‘duration’ function present in the originalTask object
is also available in the copiedTask
object. The type of ‘date’ is an object in both originalTask
and copiedTask
.
Conclusion
This post has given you a comprehensive guide on carrying out a deep copy and shallow copy in Javascript. Below is a summary of methods that you can use to perform a deep copy for different scenarios.
Making a deep copy of un-nested objects.
- Spread Operator
- Object.stringify
Making a deep copy of nested objects.
- JSON.parse(JSON.stringify()) methods : Remember, this will not work for objects that have methods as values.
- Third-party libraries (i.e Lodash)
Was this article helpful? Do you have any comments or suggestions?
Let us know by replying on Twitter of Become A Better Programmer or to my personal Twitter account.