Learn about Mutability and Immutability in JavaScript with the help of code examples. This tuorial will help you to fully grasp the concept of mutability and immutability of data in JavaScript.
In JavaScript, different data types have different behaviors and locations in memory. So to reduce the chances of having bugs in your code, you need to understand the concept of mutability and immutability in JavaScript.
Mutability refers to data types that can be accessed and changed after they've been created and stored in memory. Immutability, on the other hand, refers to data types that you can't change after creating them – but that you can still access in the memory.
This article will help you to fully grasp the concept of mutability and immutability of data in JavaScript. We'll begin with understanding the different data types and go from there.
Data Types in JavaScript
Data types are categorized into Primitive
and Reference
types in JavaScript. Before explaining these categories, let's look at two important terms with regards to memory that you will need to know: the Stack
and Heap
.
Stack is a data structure that obeys the Last In First Out
(LIFO) principle. This implies that the last item to enter the stack
goes out first.
Imagine a pile of books stacked up on a shelf. The last book ends up being the first to be removed. Data stored inside the stack can still be accessed easily.
Reference data are stored inside the heap
. When reference data is created, the variable of the data is placed on the stack
but the actual value is placed on the heap
.
Primitive data types are immutable and are not objects because they lack properties and methods.
To determine the kind of data you are working with, use the typeof
operator. The typeof
operator works perfectly with all primitive data types except null
.
Let's look at some examples of primitive data types now to get a better understanding of what they are and how they work
Here's an example of a number:
let num = 23;
console.log(typeof num)
Here's an example of a string:
let str = "Table"
Here's an example of an undefined variable. A variable is said to be undefined if there are no values attached to it.
let figure;
`null`
let fig = null
console.log(fig)
console.log(fig === null)
Keep in mind that null
is not same as Null
or NULL
.
Here's an example of a boolean. This primitive data type is either true
or false
.
let student = true;
let married = false;
Booleans are not strings – notice that true
or false
are not in quotes.
Here's an example of a symbol. As a primitive data type, symbols are unique. The values that are returned are also guaranteed to be unique.
const mySymbol = Symbol();
console.log(typeof mySymbol) //Symbol
Here's an example of BigInit. Use BigInt
when the values you are working on are too big for the number data types.
const myBigInt = 12n;
console.log(typeof myBigInt) //BigInt
const check = BigInt(414242532)
console.log(typeof check)
By default, reference data types are mutable. Reference data types consist of Functions
, Arrays
, and Objects
.
Let's look at some examples of reference data types to help you understand better:
Here's an example of a function:
function favorite(question) {
console.log(`Hi dear, do you like ${question} programming language?`)
}
favorite('JavaScript')
Here's an example of an array:
const countriesVisited = ['Nigeria', 'Japan', 'Australia']
console.log(countriesVisited)
Here's an example of an object:
const touristData = {
firstname: 'Camila',
lastname: 'Pedro',
Nationality: 'Spanish'
}
console.log(touristData)
Just for clarity, the firstname is called the key
while Camila is the value
.
Reference data types place the variable on the stack
. The variable serves as a pointer that points to the object
located on the heap.
The main distinction between these categories is that Primitives are immutable
but References are mutable
. Now, let's get to the meat of the matter.
What is Mutability in JavaScript?
If a data type is mutable, that means that you can change it. Mutability allows you to modify existing values without creating new ones.
For every object
, a pointer is added to the stack
, and this pointer points to the object
on the heap
.
Take, for example, the following code:
const staff = {
name: "Strengthened",
age: 43,
Hobbies: ["reading", "Swimming"]
}
On the stack you will find staff
which is a pointer to the actual object on the heap
.
const staff2 = staff;
console.log(staff);
console.log(staff2);
Another pointer is placed on the stack
when staff
was assigned to staff2
. Now, these pointers point to a single object on the heap
.
Reference data does not copy values, but rather pointers.
staff2.age = 53;
console.log(staff)
console.log(staff2)
Changing the age
of staff2
updates the age
of the staff
object. Now you know it is because both point to the same object.
You can clone the properties of an object using the Object. assign()
method and the spread
operator. With these, you can change the properties of the cloned object without changing the properties of the object from which it was cloned.
Object.assign()
method WorksThe object.assign
method copies properties from an object (the source) into another object (the target) and returns the modified target object.
Here's the syntax:
Object.assign(target, source)
The method has two arguments, target
and source.
The target
is the object that receives new properties, while the source
is where the properties come from. The target
can be an empty object {}.
In a situation where the source
and target
share the same key
, the source
object overwrites the value of the key
on the target.
const staff = {
name: "Strengthened",
age: 43,
Hobbies: ["reading", "Swimming"]
}
const staff2 = Object.assign({}, staff);
The properties on the staff
object were cloned into an empty target
.
staff2
now has its own properties. You can prove this by changing the value of any of its properties. Making this change will not affect the values of the properties on the staff
object.
staff2.age = 53;
console.log(staff)
console.log(staff2)
The value of staff2.age
that was changed to 53
does not in any way affect the value of staff.age
because they both have their own properties.
Spread
Operator WorksHere's the syntax of the spread operator:
const newObj = {...obj}
Using the spread
operator is quite simple. You need to place three dots ...
before the name of the object whose properties you intend to clone:
const staff = {
name: "Strengthened",
age: 43,
Hobbies: ["reading", "Swimming"]
}
const staff2 = {...staff};
staff2.age = 53;
console.log(staff)
console.log(staff2)
Immutability is the state where values are immutable (that is, not able to be changed). A value is immutable when altering it is impossible. Primitive data types are immutable, as we discussed above.
Let's look at an example:
const num = 46;
const newNum = num;
Looking at the code above, num
was reassigned to newNum.
Now both num
and newNum
have a value of 46
. Changing the value on newNum
will not alter the value on `num.
let student1 = "Halina";
let student2 = student1;
In the code above, a variable called student1
was created and assigned to student2
.
student1 = "Brookes"
console.log(student1);
console.log(student2)
Changing student1
to Brookes
does not change the initial value on student2
. This proves that in primitive data types, actual values are copied, so both have their own. On the stack memory, student1
and student2
are distinct.
The stack obeys the Last-In-First-Out
principle. The first item that enters the stack is the last item to go out and vice versa. Accessing items stored in the stack is easy.
So far you have learned that objects are mutable by default.
const studentNames = {
student1: 'Halina',
student2: "Brookes",
student3:"Anthony"
}
Object.defineProperty(studentNames, "student4", {
value: "Mirabel",
})
console.log(studentNames);
Now we've added student4
.
To prevent object mutability
, you can use the Object.preventExtensions()
, Object.seal()
, and Object.freeze()
methods.
For all three methods, we will explore adding properties using dot notation and the define
property, modifying properties using defineProperty, and deleting properties.
This will give you a better understanding of the capabilities and limitations of each method, and ultimately help you in determining which method may be best suited for a particular use case.
So, let's dive in and explore these methods in more detail.
Object.preventExtensions
MethodHere's the syntax of this method:
Object.preventExtensions(obj)
Using Object.preventExtensions
stops new properties from entering the object. The object does not increase in size and maintains its properties. By default, all objects in JavaScript are extensible. With this method, you can delete properties from your object.
dot notation
: const makeNonExtensive = {
firstname: "Charles",
lastname: "Chandlier"
}
Object.preventExtensions(makeNonExtensive)
makeNonExtensive.designation = "Software Engineer";
console.log(makeNonExtensive)
Check the console – the designation
property was not added and there's no error message.
const obj = {
firstname: "Derek",
designation: "Software Engineer"
}
defineProperty
methodHere's the syntax:
Object.defineProperty(obj, prop, descriptor)
Here's what's going on in that code:
obj
: The object you want to add properties to.prop
: You define the name of the property you want to add or change. It should be either a string or symbolDescriptor
: You include the value of the property. const makeNonExtensive = {
firstname: "Charles",
lastname: "Chandlier"
}
Object.preventExtensions(makeNonExtensive)
Object.defineProperty(makeNonExtensive, "age", {
value: "twenty",
})
console.log(makeNonExtensive)
index.js:361 Uncaught TypeError: Cannot define property age, object is not extensible
.define Property
const makeNonExtensive = {
firstname: "Charles",
lastname: "Chandlier"
}
Object.preventExtensions(makeNonExtensive)
Object.defineProperty(makeNonExtensive, 'firstname', {
value: 'Jason',
})
console.log(makeNonExtensive)
The value of the property of a non-extensible object can be changed as demonstrated with the above line of code.
Here's the syntax:
delete object.propertyname
const makeNonExtensive = {
firstname: "Charles",
lastname: "Chandlier"
}
Object.preventExtensions(makeNonExtensive)
delete makeNonExtensive.lastname
console.log(makeNonExtensive)
In spite of the object being non-extensible, the lastname
property was deleted.
Object.seal()
All objects in Javascript are extensible by default. Just as the name suggests, this method seals an object. You cannot add new properties to a sealed object or delete an existing property from a sealed object. But object.seal
permits modifying existing properties.
Here's the syntax:
Object.seal()
const studentNames = {
student1: 'Halina',
student2: "Brookes",
student3:"Alina"
}
Object.seal(studentNames)
console.log(Object.isSealed(studentNames))
Object.isSealed(studentNames)
is used to check if an object is sealed.
dot notation
studentNames.student4 = "Barbara";
console.log(studentNames)
Without producing an error, the dot notation fails when adding the new property student4
.
defineProperty
method const studentNames = {
student1: 'Halina',
student2: "Brookes",
student3:"Alina"
}
Object.seal(studentNames)
Object.defineProperty(studentNames, 'student4', {
value: 'Barbara'
})
console.log(studentNames)
The error message "Uncaught TypeError: Cannot define property student4, the object is not extendable" is thrown when attempting to add the same property using the define property
method.
define Property
const studentNames = {
student1: 'Halina',
student2: "Brookes",
student3:"Alina"
}
Object.seal(studentNames)
Object.defineProperty(studentNames, 'student2', {
value: "Water-Brookes",
})
console.log(studentNames)
Now student2
has been changed from "Brookes" to "Water-Brookes".
const studentNames = {
student1: 'Halina',
student2: "Brookes",
Student3:"Alina"
}
Object.seal(studentNames)
delete studentNames.student1
console.log(studentNames)
Properties cannot be removed from sealed objects. In the console, student1 still remains.
Object.freeze()
Here's the syntax:
Object.freeze()
The Object.freeze()
method freezes an object. Using this method guarantees high integrity by ensuring that pulling out, modifying existing properties, or adding new properties to the object will not be possible.
To check if an object is frozen, use the syntax below:
Object.isFrozen(obj);
Even when you apply the object.freeze
to an object, you can add new property, modify an existing property, or delete properties from objects nested under it.
Just as we have done for other methods, let's explore the object.freeze method in relation to adding new properties, modifying values, or deleting properties from an object.
dot notation
const teamplayers = {
player1: "Andrey",
player2: "Abundance"
}
Object.freeze(teamplayers)
teamplayers.player3 = "Finder";
console.log(teamplayers)
Notice that player3
was not added.
defineProperty
method const teamplayers = {
player1: "Andrey",
player2: "Abundance"
}
Object.freeze(teamplayers)
Object.defineProperty(teamplayers, 'player3', {
value: 'Charis'
})
console.log(teamplayers)
console.log(teamplayers)
Dot notation fails silently when trying to add a property, but defineproperty
throws a TypeError instead.
const teamplayers = {
player1: "Andrey",
player2: "Abundance"
}
Object.freeze(teamplayers)
teamplayers.player1 = "Christabel"
console.log(teamplayers)
This will fail silently. But with the define property below a typeError
is thrown.
const teamplayers = {
player1: "Andrey",
player2: "Abundance"
}
Object.freeze(teamplayers)
Object.defineProperty(teamplayers, 'player1', {
value: "Anne"
})
console.log(teamplayers)
Uncaught TypeError: Cannot redefine property: player1
const teamplayers = {
player1: "Andrey",
player2: "Abundance"
}
Object.freeze(teamplayers)
delete teamplayers.player2
console.log(teamplayers)
Attempting to delete a property on a frozen object also fails silently.
const teamplayers = {
player1: "Andrey",
player2: "Abundance",
substitutes: {
player3: "Jeremiah",
player4: "Jayden"
}
}
const squad = teamplayers;
Object.freeze(teamplayers)
Object.defineProperty(teamplayers.substitutes, 'player5', {
value: "Woodland"
})
console.log(teamplayers)
Player5 has been added to the nested substitutes
even though the object.freeze
method was applied to the parent teamplayers
.
You can also modify the value of the properties in the nested object.
delete teamplayers.substitutes.player3
console.log(teamplayers)
Player3 has been removed. Everything that the object.freeze prevents on the parent object is obtainable on the child object that is nested.
To prevent this, we employ the deep freeze technique as shown below:
const deepVal = obj => {
Object.keys(obj).forEach(prop => {
if (typeof obj[prop] === 'object') deepVal(obj[prop]);
});
return Object.freeze(obj);
};
const teamplayers = deepVal( {
player1: "Andrey",
player2: "Abundance",
substitutes: {
player3: "Jeremiah",
player4: "Jayden"
}
}
)
const squad = teamplayers;
Object.freeze(teamplayers)
console.log(Object.isFrozen(teamplayers));
console.log(Object.isFrozen(squad));
const deepVal = obj => {
Object.keys(obj).forEach(prop => {
if (typeof obj[prop] === 'object') deepVal(obj[prop]);
});
return Object.freeze(obj);
};
const teamplayers = deepVal( {
player1: "Andrey",
player2: "Abundance",
substitutes: {
player3: "Jeremiah",
player4: "Jayden"
}
}
)
Object.freeze(teamplayers)
Object.defineProperty(teamplayers.substitutes, 'player5', {
value: "Alice"
})
console.log(teamplayers)
Now when you attempt adding a property, you will get this error Uncaught TypeError: Cannot define property player5, object is not extensible
Also deep freeze prevents you from changing and deleting properties of an object.
A variable declared using the let
keyword can be reassigned using the assignment operator (=
). Take a look at the code below to understand what I mean.
let num = 34;
num = 50;
console.log(num);
Here, after declaring variable num
using the let
keyword, the value was reassigned from 34 to 50.
However, you cannot achieve the same thing on the same variable declared using the const
keyword.
const num = 34;
num = 50;
console.log(num);
You will get this error Uncaught TypeError: Assignment to constant variable
.
But that is not the case with objects. An object that you declared using const
is still mutable, so you can still modify the properties of that particular object as you can see below:
const getObj = {
color1: "Green",
color2: "Blue",
color3: "Yellow"
}
getObj.color1 = "Brown";
console.log(getObj.color1)
The value of color1
was altered from Green
to Brown
, even when declared with const
.
You've now learned about the various data types and whether they are immutable or mutable by default.
Objects can be changed by default. But using specific methods like the Object.seal, Object.freeze, and preventExtensions can prevent mutability.
The level of immutability provided by these methods varies, so make sure you use the one that corresponds to the integrity level you want to accomplish. Until next time, keep exploring JavaScript.
Source: https://www.freecodecamp.org
#javascript