How to Create JavaScript Shallow Copy vs Deep Copy: A Beginner's Guide

When learning JavaScript or any other programming language, you must have come across the statement that primitive data types are immutable and non-primitive data types are mutable.
This article will explain what mutability and immutability are, how to create a copy of mutable data in javaScript and real world use case of immutability.
What is Immutability in JavaScript?
Immutability in JavaScript simply means that a value cannot be changed after creating it. This means the string, number or boolean value we are assigning to a variable cannot be modified directly. Examples of immutable data include primitive data types like:
Number
String
Boolean
Null
Undefined
Symbol
Here is an example:
let girl = "Ada";
girl[0] = "O";
console.log(girl) // "Ada"
In the example above, we attempt to modify the first character of the string "Ada" by assigning a new value "O" to index 0. Unfortunately, instead of printing "Oda", it prints the original string "Ada". This is because primitive data types are immutable. Let's see another example:
let girl = "Ada";
girl = "Oda";
console.log(girl); // "Oda"
Did you notice that re-assigning the entire string "Oda" to the variable girl now prints "Oda" to the console? This works because an entirely new string was created in the computer memory, nothing was mutated. The variable named girl (which is just a pointer to a specific address in the computer memory) still remains the same but it now points to another address in memory.
What is Mutability in JavaScript
In JavaScript mutability means that a value can be changed after it is created. Examples of mutable data types include:
Objects
Arrays
let person = {
name: "Victor",
department: "Computer Science",
level: "300L",
age: 23
}
person.age = 30;
console.log(person) // person = {name:"Victor",department:"computer Science", level:"300L", age:30}
In the example above, the object person has an age property and changing the age property updates the existing object instead of creating a new object unlike immutable data which will create another address in memory.
Why Developers Create a Copy of a Mutable Data in JavaScript
Before we dive into how to create a copy of a mutable object or array in Javascript, you should understand that developers create a copy of a mutable data so that changes made to the copy won't affect the original data. A real-world example is the undo feature you use in apps like Capcut or Canva, the undo feature often works by keeping an earlier copy of the data. You easily "undo" and go back to the former data. When you mutate without creating a copy, the changes made will affect the original data.
Trying to Mutate Without Creating a Copy
let language = {
name: "JavaScript",
level: "High-level language"
}
let copiedLanguage = language;
console.log(copiedLanguage) // { name: "JavaScript", level: "High-level language"}
copiedLanguage.name = "Python"
console.log(language); //{name: "Python", level: "High-level"}
console.log(copiedLanguage); //{name: "Python", level: "High-level"}
In the example above, we are creating an object language. We are storing language in another variable copiedLanguage. That automatically makes copiedLanguage an object too. We want to modify the name property of copiedLanguage from "JavaScript" to "Python" and unfortunately, the name property of the original data language also changes too. This is because assigning language to copiedLanguage means both variables reference the same object in memory.
How to Create Copies for Objects and Arrays
In JavaScript, you will always create copies for objects and arrays. JavaScript has several ways of creating shallow and deep copies for objects and arrays. Shallow copy only copies the top level of an object or array. If the object or array contains a nested object or array, then a shallow copy will only copy the nested array's or object's reference without copying the actual value in the nested object or array. This means that changes made to the nested object or array in the copied data will also affect the nested object or array in the original. We will use an example to explain this in detail shortly.
Ways to Create Shallow Copy for Objects and Arrays
Let's explore some of the ways to create copies for objects and arrays.
Using the Spread Operator
One way for creating a copy of an object or an array is using the spread operator(...). Here's an example of how to use the spread operator(...) to create a shallow copy for an object:
let firstPerson = {
name: "Victor",
age: 23
};
let secondPerson = {...firstPerson};
secondPerson.name = "Alex";
console.log(firstPerson) //{name: "Victor", age: 23}
console.log(secondPerson) //{name: "Alex", age: 23}
In this example above, we're using the spread operator to create a copy of the object firstPerson and storing it in another variable secondPerson. Notice that modifying the name property of the object secondPerson from "Victor" to "Alex" did not change the name property of the original object firstPerson. Here's an example on how to create a shallow copy for an array:
const number = [1, 2, 3];
const copiedNumber = [...number];
copiedNumber[1] = 3;
console.log(number); // [1, 2, 3]
console.log(copiedNumber); // [1, 3, 3]
In the example above, we're using the spread operator to create a copy of the number array and storing it in another variable copiedNumber. Now, changes made to the copy won't affect the original array.
let myData = {
name:"Tochi",
"other info": {
sex: "male",
age: "24"
}
};
let myCopiedData = {...myData};
myCopiedData["other info"].age = 30;
console.log(myData["other info"].age); // 30
console.log(myCopiedData["other info"].age); // 30
In the example above, we are creating an object myData, which is nesting another object other info in it. We are also creating a shallow copy of myData and naming it myCopiedData. Remember Shallow copy only copies the top level of an object or array. In this example, the top level of the object myData is all the properties and values of myData excluding the other info which is an object nested in myData. Notice that changing the value of age to 30 in myCopiedData is affecting the value of age in myData and updating it to 30. This is happening because age is a property of the object other info, and other info is an object nested in the object myData. This demonstrates that a shallow copy only copies the top level of an object.
Using the Object.assign Method
The Javascript assign method allows you to merge two or more objects together. The Object.assign method takes two or more arguments. If the assign method is taking two arguments, the first argument is the object that receives properties, while the second argument is the object sending the properties.Here's an example:
let firstPerson = {name: "Victor"};
let secondPerson = {department: "Computer Science"};
let fullDetail = Object.assign(firstPerson, secondPerson);
console.log(fullDetail); // {name: "Victor",department: "Computer Science"}
To create a copy of an object using the Object.assign method, use an empty object as the first argument and the object you want to copy as the second argument. Here's an example:
let developer = {name: "Victor", role: "Front-end developer", age: 23};
let anotherDeveloper = Object.assign({}, developer);
anotherDeveloper.role = "Back-end developer";
console.log(developer); // {name: "Victor", role: "Front-end developer", age: 23}
console.log(anotherDeveloper); // {name: "Victor", role: "Back-end developer", age: 23}
In the example, we are creating a copy of the developer object using Object.assign and we are naming it anotherDeveloper. We can make changes to anotherDeveloper without affecting the original developer object.
Using the slice Method
The slice method is an array method used to copy an array without modifying the original data. To use the slice() method, call the slice method on the array you want to copy.
let availableTechRoles = ["frontend engineer", "backend engineer", "fullstack engineer"];
let notAvailableTechRoles = availableTechRoles.slice();
console.log(availableTechRoles); // ['frontend engineer', 'backend engineer', 'fullstack engineer']
console.log(notAvailableTechRoles); // ['frontend engineer', 'backend engineer', 'fullstack engineer']
In the example above, we are creating another copy of the availableTechRoles array using the slice() method and naming it notAvailableTechRoles.
How to Create a Deep Copy in JavaScript
Deep copy simply means creating a copy of an object or array in which every level of nested data is duplicated or cloned. A deep copy creates entirely new copies of all nested objects and arrays. Here's how to create a deep copy:
Using structuredClone()
One of the most efficient ways of creating a deep copy is using structuredClone(). structuredClone() takes only one argument which is the object or array it is duplicating. Here's an example:
let myData = {
name:"Tochi",
"other info": {
department: "computer science",
sex: "male",
age: "24"
}
};
let myCopiedData = structuredClone(myData);
myCopiedData["other info"].age = 40;
console.log(myData["other info"].age); // 24
console.log(myCopiedData["other info"].age); // 40
In the example above, we are creating a deep copy using structuredClone() and storing it in myCopiedData. We are passing myData as the argument of structuredClone() because it is the object we are duplicating. Notice that changing the value of age in myCopiedData did not modify the value of age in original object myData. This is because the copy myCopiedData does not share a nested object with the original myData.
Using JSON Method for Deep Copy
Before structuredClone() became widely available, developers often used the following approach to create a deep copy of an object in JavaScript:
const copiedData = JSON.parse(JSON.stringify(originalData));
This method converts the object into a JSON string and then converts it back into a JavaScript object. Let's see how it works:
JSON.stringify(originalData)converts the JavaScript object into a JSON string.JSON.parse()converts the JSON string back to an object.
It is important to note that using the JSON Method for deep Copy has some limitations, therefore structuredClone() is generally the better option.
Real-World Use Cases of Immutability
Relating what you're learning with where it is applied in real life can help you understand the concept better. Remember, immutable data is never changed directly. Instead, you create a new copy with the updates. Here are some examples of applying immutability in the real world:
Undo Feature in Canva or CapCut
Let's assume you are editing a picture. Instead of making changes to the original permanently, the app keeps previous versions. Here's a code example to demonstrate this:
const originalPic = {
brightness: 50,
contrast: 3
};
const editedPic = {
...originalPic,
brightness: 70
};
console.log(originalPic.brightness); // 50
console.log(editedPic.brightness); // 70
Google Docs Version History
When you edit a document, systems often keep previous versions.
Version 1 → Version 2 → Version 3
If something goes wrong, you can return to an earlier version. This relies on the idea of preserving old states.
What You've Learned
In this article, you learned the fundamentals of mutability and immutability in JavaScript. You explored why primitive data types are immutable, while objects and arrays are mutable, and how this difference affects the way data is stored and modified in memory.
You also learned why developers create copies of mutable data to prevent unintended changes to the original data. The article covered the distinction between shallow and deep copies, demonstrating how shallow copies duplicate only the top level of an object or array, while deep copies duplicate every level of nested data.
Finally, you explored several techniques for creating copies in JavaScript, including the spread operator, Object.assign(), slice(), structuredClone(), and JSON methods. By understanding these concepts and their real-world applications, such as undo functionality and version history systems, you can manage data more predictably and avoid common bugs when working with objects and arrays.
