Immutability in simple words is the quality of being unchangeable.
In JavaScript, we have 2 types of data - Primitives and Objects.
Lets list down all the primitives, shall we -
- String
- Number
- Boolean
- null
- undefined
- BigInt
- Symbol
And yes, null
is also a primitive, even though when we do typeof null
, it gives us object
. Trust me, it’s a primitive.
Here comes the kicker - All primitive data types of JavaScript are immutable. Which simply means, they can’t be changed.
Deal with const
Now you must be thinking, when I said “They can’t be changed”, that it seems invalid, because we can have a code like -
let a = 10;
a = 20;
console.log(a); // It prints 20
So you think that it can be changed then, right ? Wrong.
a
was holding primitive 10
with it initially, but now we have reassigned a
to hold another primitive 20
. Get it ?
JavaScript did not replace the value 10
in its place to be 20
. Instead it created one more value 20
and assigned it to variable a
.
Now let’s consider the example of const
-
const a = 10;
a = 20; // We can't do that right
We all know we can’t reassign a
as it is declared via const
, but let me tell you - That has nothing to do with immutability. Declaring anything with const
doesn’t mean that we are making it immutable, it simply means that reassignment of that variable is prohibited.
Immutability in Arrays
Let’s consider a very basic array defined in this way -
const a = [1, 2, 3, 4, 5];
Here variable a
doesn’t hold the actual array of these numbers. Rather it holds the address of the location wherever this array is stored in memory. So if we do -
const a = [1, 2, 3, 4, 5];
const b = a;
b.push(6);
console.log(a);
Even though a new value was pushed via b
, the new element 6
gets added to the array stored at a
because when we did const b = a;
, we essentially assigned the address of array pointed to by a
to b
, so now b
points to the same array. Hence the console output from the above code shows -
[1, 2, 3, 4, 5, 6];
Mutating Array Methods
You saw in the above example that push
Array method mutated or changed the array in its place. Such methods are called Mutating Array Methods.
These are - fill()
, copyWithin()
, pop()
, push()
, reverse()
, shift()
, sort()
, splice()
and unshift()
.
Non-mutating Array Methods
There are those methods of arrays which don’t change the contents of array in its place, rather it returns a new array based on the operation that needs to be carried out by the method. It does not modify the original array.
These are - concat()
, slice()
, join()
, toString()
, indexOf()
, lastIndexOf()
, includes()
, every()
, some()
, filter()
, map()
, reduce()
and reduceRight()
.
Spread Operator [...]
One way we can keep the original array intact even while using Mutating Array Methods is by making a copy of the array first via spread operator, and then, making changes on the copy version.
function addAnElement(arr, val) {
const arr2 = [...arr]; // a copy of array
arr2.push(val);
return arr2;
}
console.log(addAnElement([1, 2, 3, 4, 5], 6)); // It prints [1, 2, 3, 4, 5, 6]
We can use the traditional Array.from()
method here as well.
Immutability in Objects
Well, actually arrays are also objects in JavaScript.
Object.freeze()
method
Whenever we think about immutability in objects, we immediately think of Object.freeze()
method. An example of this -
const a = {
name: "Sanjeet",
role: "Developer",
};
Object.freeze(a);
a.role = "Speaker"; // This will give an error
This will give out an error stating something like - Cannot assign to read only property ‘role’ of object. It’s because the object a
is frozen and the properties of the object can’t be modified directly like in the above example.
It can be used with arrays as well though -
const a = [1, 2, 3, 4, 5];
Object.freeze(a);
a[1] = 6; // This will also give out an error
Small problem with Object.freeze()
Object.freeze()
method only does shallow freezing of the object. No worries, I’ll explain. Consider the below code.
const a = {
name: {
first: "Sanjeet",
last: "Tiwari",
},
role: "Developer",
};
Object.freeze(a);
a.name.first = "Awesome"; // This will NOT give an error
console.log(a);
As you can see, only first properties of the object are frozen.
Deep freezing of an Object
A function can be written to perform deep freeze on an object wherein we can iterate over properties of the object recursively and freeze them using Object.freeze()
method.
function deepFreeze(obj) {
Object.keys(obj).forEach((objKey) => {
if (
typeof obj[objKey] === "object" &&
obj[objKey] !== null &&
!Object.isFrozen(obj[objKey])
) {
deepFreeze(obj[objKey]);
}
});
return Object.freeze(obj);
}
Very Simple! So, now if we do -
const a = {
name: {
first: "Sanjeet",
last: "Tiwari",
},
role: "Developer",
};
deepFreeze(a);
a.name.first = "Awesome"; // This WILL give out an error
Proxy Object
We can also get immutable proxies of objects using Proxy
utility in JavaScript which gives us extra control over its properties.
One really great thing about using proxies is the ability to only turn some properties of the object immutable.
function turnImmutable(obj, propertiesToFreeze = []) {
return new Proxy(obj, {
get: (target, prop) => {
if (
typeof target[prop] === "object" &&
target[prop] !== null &&
!Object.isFrozen(target[prop])
) {
return turnImmutable(target[prop]);
}
return target[prop];
},
set: (target, prop, newVal) => {
if (propertiesToFreeze.length === 0 || propertiesToFreeze.includes(prop))
throw new Error("Object is immutable");
target[prop] = newVal;
return true;
},
});
}
const a = {
name: "Sanjeet",
address: "Nagpur",
};
const newA = turnImmutable(a, ["address"]);
newA.name = "Awesome";
newA.address = "Bengaluru"; // This WILL give out an error
// because we passed address as the property to freeze
The above example will allow you to change the name
property but NOT the address
property as we had passed address
in the propertiesToFreeze
parameter which doesn’t let anyone modify the property.
Why is Immutability important ?
Well, mutating mutable entities in JavaScript can lead to unintended consequences. You can imagine, right ? If there’s an important document that you want to update, you’ll keep an original copy of it handy, right ?
Because, in very large code bases, a small thing as simple as a variable update might trigger some bugs at places it was referenced earlier. So, better play it safe.