DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 966,904 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for Working with immutable arrays and objects in Javascript
CΓ‘ssio Lacerda
CΓ‘ssio Lacerda

Posted on • Updated on

Working with immutable arrays and objects in Javascript

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.

const userName = "Walter White";
userName = "Jesse Pinkman"; // error: Assignment to constant variable. 
Enter fullscreen mode Exit fullscreen mode

Learn more about let and const differences.

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 πŸ™ƒ.

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:

const seasons = 5;
seasons = 3; // error: Assignment to constant variable. 
Enter fullscreen mode Exit fullscreen mode
const isFinished = true;
isFinished = false; // error: Assignment to constant variable. 
Enter fullscreen mode Exit fullscreen mode

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

const enemies = ["Jack Welker", "Gus Fring", "Tuco"];
enemies.push("Mike");
console.log(enemies); // ['Jack Welker', 'Gus Fring', 'Tuco', 'Mike']
Enter fullscreen mode Exit fullscreen mode
const user = {name: "Walter White", profession: "Teacher"};
user.profession = "Drug dealer";
console.log(user); // {name: 'Walter White', profession: 'Drug dealer'}
Enter fullscreen mode Exit fullscreen mode

What?

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

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. 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, 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.

const enemies = Object.freeze([
   "Jack Welker", 
   "Gus Fring", 
   "Tuco"
]);
enemies.push("Mike"); // error: Cannot add property 3
Enter fullscreen mode Exit fullscreen mode

In array case, an error stops the execution! It works. Now, let's try the same with an object:

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' }
}
*/
Enter fullscreen mode Exit fullscreen mode

In object case, no errors occurs and something looks strange:

πŸ™‚ user.profession is unchanged.

😒 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:

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' }
}
*/
Enter fullscreen mode Exit fullscreen mode

From now on, any change will require to create a new object or array:

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' }
Enter fullscreen mode Exit fullscreen mode
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' ]
Enter fullscreen mode Exit fullscreen mode

Immutable.js

One last tip, you can also use the Immutable.js 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.

Top comments (0)

This post blew up on DEV in 2020:

js visualized

πŸš€βš™οΈ JavaScript Visualized: the JavaScript Engine

As JavaScript devs, we usually don't have to deal with compilers ourselves. However, it's definitely good to know the basics of the JavaScript engine and see how it handles our human-friendly JS code, and turns it into something machines understand! πŸ₯³

Happy coding!