Working with immutable arrays and objects in Javascript
Tags: javascript, arrays, objects, immutability
Published: 2021-11-01 12:57:07

When `let` and `const` keywords were introduced in ES2015 (ES6), many of declaring problems in javascript variables were solved. In addition to block scoping improvement, also encountered in `let` declarations, `const` could ensure the variable was **declared only once and its value was not modified later**. 

```js
const userName = "Walter White";
userName = "Jesse Pinkman"; // error: Assignment to constant variable. 
``` 
> *Learn more about [let and const differences](https://www.freecodecamp.org/news/var-let-and-const-whats-the-difference/).*

If you are wondering, why should I use **constants** in my coding? 

Here are some reasons: 

1. It protects yourself, avoiding scenarios where accidental assignment happens;
2. It makes code more readable;
3. It optimizes the memory;

In short, it's good practice to use them &#x1F643;. 

Although `const` keyword had been a great improvement to use constants in Javascript, it's not the silver bullet for immutability as maybe you think...

## Primitives vs non-primitives data types

We have been using a `string` as data type in `const` declaration to thrown an error at runtime. Other primitive data types like `number` and `boolean` behave the same way:

```js
const seasons = 5;
seasons = 3; // error: Assignment to constant variable. 
``` 
```js
const isFinished = true;
isFinished = false; // error: Assignment to constant variable. 
```

But now, try changing data of a non-primitive data type like `array` and `object`:

```js
const enemies = ["Jack Welker", "Gus Fring", "Tuco"];
enemies.push("Mike");
console.log(enemies); // ['Jack Welker', 'Gus Fring', 'Tuco', 'Mike']
```

```js
const user = {name: "Walter White", profession: "Teacher"};
user.profession = "Drug dealer";
console.log(user); // {name: 'Walter White', profession: 'Drug dealer'}
```

![What?](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wc9y2lb3dfekz3wthlhl.gif)

## No errors, why?

Basically, Javascript uses **call stack** memory space to save references and values for primitives data types, while for non-primitive ones, it uses a separate space in memory called **heap**. In that case, call stack saves as value in its memory **only heap memory reference ID, not the object and array values**. 

![call stack and heap](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ltgf2mtzxtlb3hcyann0.png)

When we add elements for arrays or change object properties values, **reference ID in call stack keeps the same and their values are changed only in heap memory, not throwing any errors**.

> *There is an amazing post created by Ethan Nam explaining in depth the [JavaScript’s Memory Model](https://medium.com/@ethannam/javascripts-memory-model-7c972cd2c239). I strongly advise you read it.*

## Blocking changes in arrays and objects

To achieve the desired goal, let's blocking changes for arrays and objects with `Object.freeze()`. Show me the [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze), please:

> *The Object.freeze() method freezes an object. A frozen object can no longer be changed; freezing an object prevents new properties from being added to it, existing properties from being removed, prevents changing the enumerability, configurability, or writability of existing properties, and prevents the values of existing properties from being changed. In addition, freezing an object also prevents its prototype from being changed. freeze() returns the same object that was passed in.* 

```js
const enemies = Object.freeze([
   "Jack Welker", 
   "Gus Fring", 
   "Tuco"
]);
enemies.push("Mike"); // error: Cannot add property 3
```
In array case, an error stops the execution! It works. Now, let's try the same with an object:

```js
const user = Object.freeze({
  name: "Walter White",
  profession: "Teacher",
  address: {
    city: "Albuquerque",
    state: "NM",
    country: "USA",
  },
});
user.profession = "Drug dealer";
user.address.country = "Brazil";
console.log(user);
/*
{
  name: 'Walter White',
  profession: 'Teacher',
  address: { city: 'Albuquerque', state: 'NM', country: 'Brazil' }
}
*/
```
In object case, no errors occurs and something looks strange: 

&#x1F642; `user.profession` is unchanged.

&#x1F622; `user.address.country` not...

Why?

## Shallow vs deep freeze

When we freeze an object, **only the top-level properties are frozen**. In other words, the nested objects properties can be changed, that's a `shallow freeze`. For `deep freeze`, we need to recursively freeze each property of type object and we can create a helper function to do it:

```js
function deepFreeze(obj) {
  Object.keys(obj).forEach((prop) => {
    const value = obj[prop];
    if (typeof value === "object") deepFreeze(value);
  });
  return Object.freeze(obj);
}

const user = deepFreeze({
  name: "Walter White",
  profession: "Teacher",
  address: {
    city: "Albuquerque",
    state: "NM",
    country: "USA",
  },
});
user.profession = "Drug dealer";
user.address.country = "Brazil";
console.log(user);
/*
{
  name: 'Walter White',
  profession: 'Teacher',
  address: { city: 'Albuquerque', state: 'NM', country: 'USA' }
}
*/
```
From now on, any change will **require to create a new object or array**:

```js
const user = Object.freeze({
  name: "Walter White",
  profession: "Teacher",
});

const newUserPropAdded = Object.freeze({
  ...user,
  age: 55,
});
console.log(newUserPropAdded);
// { name: 'Walter White', profession: 'Teacher', age: 55 }

const newUserPropUpdated = Object.freeze({
  ...user,
  profession: "Drug dealer",
});
console.log(newUserPropUpdated);
// { name: 'Walter White', profession: 'Drug dealer' }

const { profession, ...newUserPropDeleted } = user;
console.log(newUserPropDeleted);
// { name: 'Walter White' }

console.log('unchanged user :>> ', user);
// unchanged user :>>  { name: 'Walter White', profession: 'Teacher' }
```
```js
const enemies = Object.freeze(["Jack Welker", "Gus Fring", "Tuco"]);
const index = 1;

const newEnemiesItemAdded = [...enemies, "Mike"];
console.log(newEnemiesItemAdded);
// [ 'Jack Welker', 'Gus Fring', 'Tuco', 'Mike' ]

const newEnemiesItemUpdated = enemies.map((item, i) =>
  i === index ? "Jesse" : item
);
console.log(newEnemiesItemUpdated);
// [ 'Jack Welker', 'Jesse', 'Tuco' ]

const newEnemiesItemDeleted = [
  ...enemies.slice(0, index),
  ...enemies.slice(index + 1),
];
console.log(newEnemiesItemDeleted);
// [ 'Jack Welker', 'Tuco' ]

console.log("unchanged enemies :>> ", enemies);
// unchanged enemies :>>  [ 'Jack Welker', 'Gus Fring', 'Tuco' ]
```

## Immutable.js

One last tip, you can also use the [Immutable.js](https://www.npmjs.com/package/immutable) library to add an easier way to work with the immutability of non-primitive data types in Javascript.

## Conclusion

Understand how non-primitive data types work is very important for handle data in the correct way in JavaScript. Its memory model based in call stack and heap are essential parts of it and you should to know it.