Hi there! It's Howard Phung here. Welcome back to Web Dev Distilled. πββοΈ
In this article, I will show you
The differences between Shallow vs Deep Copy
The problem with Shallow Copy
Implement Deep Copy Function just by Vanilla JavaScript (without any library)
But...
First, let's review some JavaScript essentials to understand the above concepts better.
If you already mastered these, please feel free to skip forward to the main part of this article.
Pass by Value and Pass by Reference
- With primitive data types example
let x = 2024;
let y = x;
y++;
console.log('π ~ x:', x); // 2024
console.log('π ~ y:', y); // 2025
Here we increase the value of y
by 1, to 2025
. But the value of x
still remains unaltered. That's a very simple and concrete example of Pass by Value.
In contrast, have a look at the 2nd example below
- With Reference (Structural) data types
let arrayA = [1, 2, 3];
let arrayB = arrayA;
arrayB.push(4);
console.log('π ~ arrayA:', arrayA); // [1,2,3,4]
console.log('π ~ arrayB:', arrayB); // [1,2,3,4]
Now in this example, we're working with the reference data types, and they use references
.
As a result, when we mutated the arrayB
, by pushing the number 4 to the end, the arrayA
got changed at the same time.
In other words, arrayA
and arrayB
shared the same reference.
Just need to understand the behavior here.
Here we made the side effect
to the original data structure. That might not be the expected behavior we want most of the time.
Especially in the world of functional programming
we've been working around the concept of pure function
, immutable data
, try to don't make side effect
. But that will be the topic for another series on my Blog soon π. Make sure you already subscribed.
Back to the course, it's gonna take me another whole post to dive deep into pass by value and by refences
in JS. If you come from another programming language, you should notice, that JS doesn't have real pass by reference
, it just passes the value
hold the reference that points to the memory address where the original data structure is stored.
In the scope of this article, this is enough for you to move on.
Mutable and Immutable Data
Primitive data types are Immutable
let myBlogName = "Web Dev Distilled";
myBlogName[0] = "M"; // π¨ CANNOT MUTATE THE ORIGINAL STRING!!!
console.log(myBlogName) // π "Web Dev Distilled"
// Reassignment is NOT THE SAME as Mutable
myBlogName = "Mobile Dev Distilled"
console.log(myBlogName) // "Mobile Dev Distilled"
Reference (or Structural) data types contain Mutable data
const arrayA = [1,2,3];
const arrayB = arrayA;
arrayB[1] = 5;
console.log(arrayA) // [1,5,3]
console.log(arrayB) // [1,5,3]
// arrayA and arrayB share the same reference
Pure function
The pure function requires avoiding directly mutating the original input we pass in the function, to not create any unexpected side-effect
Here is an example of a impure function that mutates the input data
const addNumToArray = (array, number) => {
array.push(number);
return array;
};
We should modify the function to return another brand new array instead and not change anything from the outsite world
of the function
const addNumToArray = (array, number) => {
const newArray = [...array];
newArray.push(number);
return newArray;
};
Here we create a brand new array newArray
from the input array
using the spread operator.
Although it seems good, there is still a problem - will occur when the original input array or object have Nested structural data type.
The spread operator just creates a SHALLOW COPY of the array, the same behavior also happens with Array.from()
and slice()
, with Object Object.assign()
- they all just create a shallow copy.
With shallow copy, nested data structure still SHARE A REFERENCE!
Turns out, in the context of functional programming, we're still creating a side effect in this case.
Bear with me, we'll get into the example to better illustrate.
Shallow Copy
With Array
// With spread operator
const arrayX = [1, 2, 3];
const arrayY = [...arrayX, 4];
console.log('π ~ arrayX:', arrayX); // [1,2,3]
console.log('π ~ arrayY:', arrayY); // [1,2,3,4]
// with Object.assign()
const arrayZ = Object.assign([], arrayY);
console.log('π ~ arrayZ:', arrayZ); // [1,2,3,4]
console.log(arrayZ === arrayY); // false
// BUT WE HAVE PROBLEM WITH NESTED arrays or objects....
arrayY.push([5, 6, 7]);
const arrayV = [...arrayY];
console.log(arrayV); // [1,2,3,4, [5,6,7]]
arrayV[4].push(8);
console.log(arrayV); // [1,2,3,4, [5,6,7,8]
console.log(arrayY); // [1,2,3,4, [5,6,7,8]
With Objects
const personObj = {
name: 'John',
age: 24,
scores: {
a: 1,
b: 2,
},
};
Object.freeze(personObj);
personObj.name = 'Howard';
console.log(personObj); // π¨ OOPS! Can't change!! My name still John
// BUT with nested Object, still CAN change
personObj.scores.a = 30;
console.log(personObj); // {name: 'John', age: 20, scores: {a : 30, b: 2}}
// => Ojbect.freeze is still just a SHALLOW freeze
Deep Copy
- One line solution
const newPersonObj = JSON.parse(JSON.stringify(personObj));
console.log(newPersonObj === personObj); // false β
newPersonObj.name = 'Howard';
console.log(newPersonObj); // {name: 'Howard', age: 20, scores: {a : 30, b: 2}}
console.log(personObj); // {name: 'John', ....}
With this quick solution, we successfully created a deep copy of the personObj
.
But we have a downside with this approach - it does not work with Dates, functions, undefined, Infinity, Regexps, Blobs, Sets, Maps...
By turning the object into string, and then turn it back using JSON.parse
, but JSON stringify will lose the type listed above.
This will be a big problem if you try to send the object through an API or store into localStorage.
- Vanilla JavaScript implementation
function deepClone(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
// Create an array or object to hold the values;
const newObject = Array.isArray(obj) ? [] : {};
for (let key in obj) {
const value = obj[key];
// recursive call for nested objects & arrrays;
newObject[key] = deepClone(value);
}
return newObject;
}
const scoreArray = [3, 4, 5, 6];
const newScoreArray = deepClone(scoreArray); // [3,4,5,6]
console.log(newScoreArray === scoreArray); // false
Now we made 100% deep copy of an array. Yayy π
newScoreArray.push([1, 2, 3]);
console.log(newScoreArray) // [3, 4, 5, 6, [1, 2, 3]];
newScoreArray.at(4).push(4);
console.log(newScoreArray) // [3, 4, 5, 6, [1, 2, 3, 4]];
console.log(scoreArray) // [3, 4, 5, 6] => remain Unchange !!!
Thanks for reading.