Deep Copy and Shallow Copy in Javascript.
What is copy?
In programming, we store values in variables. Making a copy means that you initiate a new variable with the same value(s).
We have two type of Copy
- Deep Copy : deep copy makes a copy of all the members of the old object, allocates separate memory location for the new object and then assigns the copied members to the new object. In this way, both the objects are independent of each other and in case of any modification to either one the other is not affected.
- Shallow Copy : A shallow copy means that certain (sub-)values are still connected to the original variable.
Problem In Shallow Copy : a reference variable mainly stores the address of the object it refers to. When a new reference variable is assigned the value of the old reference variable, the address stored in the old reference variable is copied into the new one. This means both the old and new reference variable point to the same object in memory. As a result if the state of the object changes through any of the reference variables it is reflected for both
We have two categories of data types in javascript
Primitive data types
Primitive data types include the following:
- Number — e.g.
1
- String — e.g.
'Hello'
- Boolean — e.g.
true
undefined
null
When you create these values, they are tightly coupled with the variable they are assigned to. They only exist once. That means you do not really have to worry about copying primitive data types in JavaScript. When you make a copy, it will be a real copy. Let’s see an example:
const a = 5
let b = a // this is the copy
b = 6console.log(b) // 6console.log(a) // 5
By executing b = a
, you make the copy. Now, when you reassign a new value to b
, the value of b
changes, but not of a
.
Composite data types — Objects and Arrays
In These values are actually stored just once when instantiated, and assigning a variable just creates a pointer (reference) to that value.
E.g
const a = { en: 'Hello', de: 'Hallo', es: 'Hola', pt: 'Olà'}let b = ab.pt = 'Oi'console.log(b.pt) // Oiconsole.log(a.pt) // Oi
In the example above, we actually made a shallow copy. This is often times problematic, since we expect the old variable to have the original values, not the changed ones.
How to handle this problem?
Spread operator
Introduced with ES2015, this operator is just great, because it is so short and simple. It ‘spreads’ out all of the values into a new object. You can use it as follows:
const a = { en: 'Hello', de: 'Hallo', es: 'Hola', pt: 'Olà'}let b = {...a}b.pt = 'Oi'console.log(b.pt) // Oiconsole.log(a.pt) // Ola
You can also use it to merge two objects together, for example const c = {...a, ...b}
.
Object.assign
This was mostly used before the spread operator was around, and it basically does the same thing.
const a = { en: 'Hi', de: 'bye' }let b = Object.assign({}, a)b.de = 'tata'console.log(b.de) // tataconsole.log(a.de) // bye
Nested Objects
const a = { foods: { dinner: 'Pasta' } }let b = {...a}b.foods.dinner = 'Soup' // changes for both objectsconsole.log(b.foods.dinner) // Soupconsole.log(a.foods.dinner) // Soup
To make a deep copy of nested objects, you would have to consider that. One way to prevent that is manually copying all nested objects:
const a = { foods: {dinner: 'Pasta'}}let b = {foods: {...a.foods}}b.foods.dinner = 'Soup'console.log(b.foods.dinner) // Soupconsole.log(a.foods.dinner) // Pasta
Making deep copies without thinking
What if you don’t know how deep the nested structures are? It can be very tedious to manually go through big objects and copy every nested object by hand. There is a way to copy everything without thinking. You simply stringify
your object and parse
it right after:
const a = {foods: {dinner: 'Pasta'}}let b = JSON.parse(JSON.stringify(a))b.foods.dinner = 'Soup'console.log(b.foods.dinner) // Soupconsole.log(a.foods.dinner) // Pasta
copying instance of custom classes
class Counter { constructor() { this.count = 5} copy()
{
const copy = new Counter();
copy.count = this.count;
return copy
}}const originalCounter = new Counter()const copiedCounter = originalCounter.copy()console.log(originalCounter.count) // 5console.log(copiedCounter.count) // 5copiedCounter.count = 7console.log(originalCounter.count) // 5console.log(copiedCounter.count) // 7