Hi, I am Sanjeet Tiwari...
Let's talk about

Back to Notes

Immutability in JavaScript

JavaScript Security

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 -

  1. String
  2. Number
  3. Boolean
  4. null
  5. undefined
  6. BigInt
  7. 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.

Video Sources

Immutability in JavaScript

Immutability in JavaScript

Steve Griffith - Prof3ssorSt3v3

Sanjeet's LinkedIn

Array Methods in JavaScript: Mutating vs Non-Mutating

Danielle Dias

Sanjeet's LinkedIn

Learn the JavaScript Proxy Object

Nick Scialli

Last updated on 23-11-2024