DEV Community

Umid Negmatullayev
Umid Negmatullayev

Posted on

Why Immutability is important in JavaScript?

In JavaScript, Primitive data types (numbers, strings, etc) are immutable but when it comes to objects and arrays they are mutable, please do not think that if you declare your objects and arrays with const it will be constant which is unchangeable:

const obj = {
  a: "apple"
}
const updatedObj = obj
updatedObj.a = "banana"
console.log(obj.a) // "banana'
Enter fullscreen mode Exit fullscreen mode

As you can see when we update updatedObj's value, it updates the original object variable obj as well. The reason behind it, objects copy by reference, which means when we do const updatedObj = obj updatedObj is referencing/pointing to obj's memory address, so if we update the updatedObj we update obj because they are pointing the same value. But in the case of primitive data types (numbers, strings, boolean, undefined, etc) is the opposite.

All primitives are immutable, i.e., they cannot be altered. It is important not to confuse a primitive itself with a variable assigned a primitive value. The variable may be reassigned a new value, but the existing value can not be changed in the ways that objects, arrays, and functions can be altered. ~ MDN

Here we can see examples where strings and numbers are not changing.

const num = 39
let updatedNum = num

updatedNum = 45
console.log(num) // 39

const str = "lion"
let updatedStr = str

updatedStr = "tiger"
console.log(str) // "lion"
Enter fullscreen mode Exit fullscreen mode

Why do we care about immutability? If JavaScript was built this way then there must be a reason. Yes, it's because JavaScript is a multiparadigm language you can use it as OOP, you can use it as FP (functional programming).
Functional programming embraces immutability and heavily practices persistent data structure. And new libraries like React and Redux take the advantages of immutability, like in Redux, store is one giant, plain JS object, immutable one and this gave the possibility for redux time travel where you can see the previous states/changes or in React you can check the previous values of your local states, they all come from the object immutability.

Here is a simple example of creating an immutable object in JS:

const obj = {
  a: "apple"
}
const updatedObj = Object.assign({}, obj)
updatedObj.a = "banana"

console.log(obj.a) // "apple"
console.log(updatedObj.a) // "banana"
Enter fullscreen mode Exit fullscreen mode

Now we do not mutate our original object obj.

We will have more practical examples on "How not to mutate your objects and arrays" in the next articles.

More about Object.assign()

You might ask a question 🙋‍♂️ , "Wait if we do not mutate our object value? Then that must be lots of memory consumptions? " ~ You are not wrong!

That's where comes structural sharing, you don't want to deep copy the object but shallow copy it. Just like git does not copy your whole versions of your code but shares the files that are not changed with the previous commit.

Object.assign() method does shallow copying. But there is one downside to it, if you have nested object properties, they will not be immutable.

const obj = {
  a: "apple",
  b: {
    c: "lemon"
  }
}
const updatedObj = Object.assign({}, obj)
updatedObj.a = "mango"
updatedObj.b.c = "banana"

console.log(obj.a) // "apple"
console.log(obj.b.c) // "banana"
Enter fullscreen mode Exit fullscreen mode

b: { c: "lemon" } is not immutable here as it's nested property, we will see examples of how to make objects and arrays immutable including nested (complex structures) ones as well.

So shallow copying will not take lots of memory consumptions.

Immutable Objects

  1. Using Object.assign()
let obj = {
  a: "apple"
}
let updatedObj = Object.assign({}, obj)
updatedObj.a = "banana"

console.log(obj.a) // "apple"
console.log(updatedObj.a) // "banana"
Enter fullscreen mode Exit fullscreen mode
  1. Using Object Spread Operators:
 let obj = {
  a: "apple"
}
let updatedObj = { ...obj }
updatedObj.a = "banana"

console.log(obj.a) // "apple"
console.log(updatedObj.a) // "banana"
Enter fullscreen mode Exit fullscreen mode

Spread Operators are new ES6 syntax, similar to Object.assign() method, it does shallow copying.

For complex data structure:

let obj = {
  a: "apple",
  b: {
     c: "lemon"
  }
}
let updatedObj = {...obj, b: { ...obj.b } };
updatedObj.a = "banana"
updatedObj.b.c = "peach"

console.log(obj.a) // "apple"
console.log(obj.b.c) // "lemon"
console.log(updatedObj.a) // "banana"
console.log(updatedObj.b.c) // "peach"
Enter fullscreen mode Exit fullscreen mode

If you have nested object properties let updatedObj = {...obj, b: { ...obj.b } }; you can do nested spread with the property name.

Immutable Array

1.Array Spread Operators

let arr = [1, 2, 3, 4]
let updatedArr = [...arr]
updatedArr[2] = 5

console.log(arr[2])// 3
console.log(updatedArr[2])// 5
Enter fullscreen mode Exit fullscreen mode

Array spread operators are the same as object spread operator, actually they are spread operators learn more here.

2.Using slice() method:

let arr = [1, 2, 3, 4]
let updatedArr = arr.slice(0, arr.length);
updatedArr[2] = 5

console.log(arr[2]) // 3
console.log(updatedArr[2]) // 5
console.log(updatedArr) // [1, 2, 5, 4]
Enter fullscreen mode Exit fullscreen mode

slice() cuts the array from the index (first argument) until the index you want (second argument), but it won't affect the original array. There is splice() array method, it's the opposite of slice() it changes the content of the original array learn more on slice here, learn more on splice.

3.Using map(), filter():

let arr = [1, 2, 3, 4]

let updatedArr = arr.map(function(value, index, arr){
  return value;
});
updatedArr[2] = 5

console.log(arr[2]) // 3
console.log(updatedArr[2]) // 5
console.log(updatedArr) // [1, 2, 5, 4]
Enter fullscreen mode Exit fullscreen mode

map() returns a new array, takes a callback function as an argument and calls it on every element of the original array. Callback function takes value(current iterated value), index (current index), array(original array) arguments, all of them are optional learn more here.

filter()

let arr = [1, 2, 3, 4]

let updatedArr = arr.filter(function(value, index, arr){
  return value;
});
updatedArr[2] = 5

console.log(arr[2]) // 3
console.log(updatedArr[2]) // 5
console.log(updatedArr) // [1, 2, 5, 4]
Enter fullscreen mode Exit fullscreen mode

filter() and map() works the same way learn more here.

They both return a new array. map() returns a new array of elements where you have applied some function on the element so that it changes the element. filter() returns a new array of the elements of the original array (with no change to the elements). filter() will only return elements where the function you specify returns a value of true for each element passed to the function.

There is one more method for array reduce(), it will not return new array, but it will do immutable operations on an original array.

let arr = [1, 2, 3, 4];
// 1 + 2 + 3 + 4
const reducer = (accumulator, currentValue) => accumulator + currentValue;

let updatedArr = arr.reduce(reducer)
console.log(updatedArr) // 10
Enter fullscreen mode Exit fullscreen mode

reduce() could be confusing at the beginning, but I will try to explain as simply as possible. Let's look at the example below:

let sum = 0;
let i = 0;
while (i<arr.length){
  sum+=arr[i]; // 1 + 2 + 3 + 4
  i++;
}

console.log(sum) // 10
Enter fullscreen mode Exit fullscreen mode

It just a loop that sums all the values of an array. We are trying to do the same thing with reduce().

reduce() takes reducer callback which is a function takes 4 arguments, accumulator, currentValue, currentIndex, originalArray. Accumulator saves the value which is returned from last iteration, just like sum variable in our loop example, current value is arr[i]. That's reduce learn more here.

I hope 🤞 it all makes sense.

Extra Resources:

This answer here gives a great explanation on "why is immutability important?",

Structural sharing,

More on immutable methods of array and object

Top comments (0)