What is Immutability?
The text-book definition of mutability isliable or subject to change or alteration.In programming, we use the word to mean objects whose state is allowed to change over time. An immutable value is the exact opposite – after it has been created, it can never change. If this seems strange, allow me to remind you that many of the values we use all the time are in fact immutable.
var statement = "I am an immutable value";
var otherStr = statement.slice(8, 17);
I think no one will be surprised to learn that the second line in no way changes the string in statement
. In fact, no string methods change the string they operate on, they all return new strings. The reason is that strings are immutable – they cannot change, we can only ever make new strings.
Strings are not the only immutable values built into JavaScript. Numbers are immutable too. Can you even imagine an environment where evaluating the expression 2 + 3
changes the meaning of the number 2
? It sounds absurd, yet we do this with our objects and arrays all the time.
In JavaScript, Mutability Abounds
In JavaScript, strings and numbers are immutable by design. However, consider the following example using arrays:var arr = [];
var v2 = arr.push(2);
What is the value of v2
? If arrays behaved consistently with strings and numbers, v2
would contain a new array with one element – the number 2 – in it. However, this is not the case. Instead, the arr
reference has been updated to contain the number, and v2
contains the new length of arr
.
Imagine an ImmutableArray
type. Inspired by strings and numbers behavior, it would have the following behavior:
var arr = new ImmutableArray([1, 2, 3, 4]);
var v2 = arr.push(5);
arr.toArray(); // [1, 2, 3, 4]
v2.toArray(); // [1, 2, 3, 4, 5]
Similarly, an immutable map, which can be used in place of most objects, would have methods to “set” properties that don’t actually set anything, but return a new object with the desired changes:
var person = new ImmutableMap({name: "Chris", age: 32});
var olderPerson = person.set("age", 33);
person.toObject(); // {name: "Chris", age: 32}
olderPerson.toObject(); // {name: "Chris", age: 33}
Just like 2 + 3
does not change the meaning of either numbers 2 or 3, a person celebrating their 33rd birthday does not change the fact that they used to be 32.
Immutability in JavaScript in Practice
JavaScript doesn’t (yet) have immutable lists and maps, so we’ll need a third-party library for now. There are two very good ones available. The first one is Mori, which enables the use of ClojureScript’s persistent data structures and supporting APIs in JavaScript. The other one is immutable.js, written by developers at Facebook. For this demonstration I will use immutable.js, simply because its API is more familiar to JavaScript developers. For this demonstration, we’ll look at how to work with Minesweeper with immutable data. The board is represented by an immutable map where the most interesting piece of data is thetiles
. It’s an immutable list of immutable maps, where each map represents a tile on the board. The whole thing is initialized with JavaScript objects and arrays, and then immortalized by immutable.js fromJS
function:
function createGame(options) {
return Immutable.fromJS({
cols: options.cols,
rows: options.rows,
tiles: initTiles(options.rows, options.cols, options.mines)
});
}
The rest of the core game logic is implemented as functions that take this immutable structure as their first argument, and returns a new instance. The most important function is revealTile
. When called, it’ll flag the tile to reveal as revealed. With a mutable data structure, that would be very easy:
function revealTile(game, tile) {
game.tiles[tile].isRevealed = true;
}
However, with the kind of immutable structures proposed above, it would turn into a bit more of an ordeal:
function revealTile(game, tile) {
var updatedTile = game.get('tiles').get(tile).set('isRevealed', true);
var updatedTiles = game.get('tiles').set(tile, updatedTile);
return game.set('tiles', updatedTiles);
}
Phew! Luckily, this sort of thing is quite common. So, our toolkit provides methods for that:
function revealTile(game, tile) {
return game.setIn(['tiles', tile, 'isRevealed'], true);
}
Now the revealTile
function returns a new immutable instance, where one of the tiles is different from the previous version. setIn
is null-safe and will pad with empty objects if any part of the key does not exist. This is not desirable in the case of the Minesweeper board, because a missing tile means we’re trying to reveal a tile outside the board. This could be mitigated by using getIn
to look for the tile before manipulating it:
function revealTile(game, tile) {
return game.getIn(['tiles', tile]) ?
game.setIn(['tiles', tile, 'isRevealed'], true) :
game;
}
If the tile does not exist, we simply return the existing game. This was a quick taste of immutability in practice, to dive deeper in, check out this codepen, which includes a full implementation of the Minesweeper game rules.
What About Performance?
You might think that this would yield terrible performance, and in some ways you’d be right. Whenever you add something to an immutable object, we need to create a new instance by copying the existing values and add the new value to it. This will certainly be both more memory intensive and more computationally challenging than mutating a single object. Because immutable objects never change, they can be implemented using a strategy called “structural sharing”, which yields much less memory overhead than you might expect. There will still be an overhead compared to built-in arrays and objects, but it’ll be constant, and can typically be dwarfed by other benefits enabled by immutability. In practice, the use of immutable data will in many cases increase the overall performance of your app, even if certain operations in isolation become more expensive.Improved Change-Tracking
One of the hardest tasks in any UI framework is mutation tracking. This is such a widespread challenge that EcmaScript 7 provides a separate API to help track object mutations with better performance:Object.observe()
. While many people are excited about this API, others feel like it’s the answer to the wrong question. In any case, it does not properly solve the mutation tracking problem:
var tiles = [{id: 0, isRevealed: false}, {id: 1, isRevealed: true}];
Object.observe(tiles, function () { /* ... */ });
tiles[0].id = 2;
The mutation of the tiles[0]
object does not trigger our mutation observer, thus the proposed mechanism for mutation tracking fails even the most trivial use-cases. How does immutability help in this situation? Given application state a
, and potentially new application state b
:
if (a === b) {
// Data didn't change, abort
}
If the application state hasn’t updated, it’ll be the same instance as before, and we don’t need do anything at all. This does require that we track the reference that holds the state, but the whole problem has now been reduced to managing a single reference.
Conclusion
I hope this article has given you some background on how immutability can help you improve your code, and that the provided example can shed some light on the practical aspects of working this way. Immutability is on the rise and this will not be the last article you read on the subject this year. Give it a shot, and I promise you’ll be as excited about it as I am in no time.Frequently Asked Questions (FAQs) about Immutability in JavaScript
What is the concept of immutability in JavaScript?
Immutability in JavaScript refers to the inability of a variable, once created, to be changed. This means that once a variable is assigned a value, it cannot be altered. This is particularly true for primitive data types in JavaScript such as strings and numbers. For instance, if you create a string and try to change a character in it, JavaScript will not allow it. Instead, it will create a new string with the desired changes, leaving the original string unaltered. This concept is crucial in functional programming and can help prevent bugs and make your code more predictable.
How does immutability differ from mutability in JavaScript?
The key difference between immutability and mutability in JavaScript lies in whether or not a variable’s value can be changed. In the case of mutability, a variable’s value can be altered after it has been assigned. This is true for objects and arrays in JavaScript. On the other hand, immutability means that a variable’s value, once assigned, cannot be changed. This is true for primitive data types like strings and numbers.
Why is immutability important in JavaScript?
Immutability is important in JavaScript for several reasons. Firstly, it makes your code more predictable and easier to understand, as you don’t have to worry about values unexpectedly changing. Secondly, it can help prevent bugs and errors, as it ensures that data cannot be changed once it has been defined. Lastly, immutability is a key concept in functional programming, a popular paradigm in JavaScript, and is necessary for implementing certain features such as pure functions and referential transparency.
How can I make an object immutable in JavaScript?
You can make an object immutable in JavaScript using the Object.freeze() method. This method prevents new properties from being added to an object, existing properties from being removed, and prevents changing the enumerability, configurability, or writability of existing properties. It essentially makes the object read-only.
Are all data types in JavaScript immutable?
No, not all data types in JavaScript are immutable. Primitive data types like strings, numbers, booleans, null, and undefined are immutable. This means that their values cannot be changed once they are created. However, non-primitive data types like objects and arrays are mutable, meaning their values can be changed.
What is the difference between shallow and deep immutability?
Shallow immutability refers to the top-level variables or properties of an object being immutable, while deep immutability means that all levels of the object, including nested objects and arrays, are immutable. In JavaScript, achieving deep immutability can be more complex and may require the use of certain libraries or techniques.
How does immutability relate to functional programming in JavaScript?
Immutability is a key concept in functional programming, a paradigm that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. In JavaScript, immutability is used to implement features of functional programming such as pure functions and referential transparency. It helps make the code more predictable, easier to test, and less prone to bugs.
Can I make an array immutable in JavaScript?
While arrays in JavaScript are mutable by default, you can make them immutable by using methods that do not change the original array but instead return a new array. These methods include map(), filter(), and reduce(). Alternatively, you can use the Object.freeze() method to make an array immutable, but this will prevent any changes to the array, including adding or removing elements.
What are the benefits and drawbacks of immutability in JavaScript?
The benefits of immutability in JavaScript include more predictable code, fewer bugs, and easier implementation of certain programming paradigms like functional programming. However, immutability can also have drawbacks. For instance, it can lead to increased memory usage, as new variables need to be created instead of modifying existing ones. It can also make certain operations more complex, as you cannot simply change a value but need to create a new variable.
How does immutability work with JavaScript frameworks like React and Redux?
JavaScript frameworks like React and Redux heavily rely on the concept of immutability. In React, the state is considered immutable, and changes to the state should be made through the setState() method, which creates a new state rather than modifying the existing one. In Redux, the state of the application is also immutable, and changes are made through actions and reducers, which return a new state. This helps ensure that the state is predictable and makes it easier to track changes and debug the application.
Christian is a programmer working with Oslo-based consultancy Kodemaker. He has an eclectic background, having worked with systems tuning and ops, server-side applications and JavaScript heavy frontend development. Christian is the author of "Test-Driven JavaScript Development", and he maintains several OSS projects, including the popular Sinon.JS.