Deep Copy & Shallow Copy with Vanilla JavaScript

Deep Copy & Shallow Copy with Vanilla JavaScript

Β·

5 min read

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.

Β