Avoiding Mutations in JavaScript with Immutable Data Structures

Kyle Shevlin
InstructorKyle Shevlin
Share this video with your friends

Social Share Links

Send Tweet

This lesson teaches you the concepts of immutability, the difference between mutable and immutable data structures, and how to change data through mutable and immutable patterns. Immutable data is necessary in functional programming because mutations are a side effect. Our transformation of data should not affect the original data source, but instead should return a new data source with our updates applied.

To demonstrate the difference between mutability and immutability, imagine taking a drink from a glass of water. If our glass is mutable, when we take a drink, we retain the same glass and change the amount of water in that glass. However, if our glass is immutable, when we take a drink, we get back a brand new, identical glass containing the correctly drank amount. Perhaps a strange way to conceive of the action, but creating new data structures makes our methods pure and thread-safe, a benefit of functional programming.

Instructor: [00:00] Mutable data structures can be changed after creations, and immutable ones cannot. Mutations can be thought of as side effects in our applications. I'm going to demonstrate this through the use of an array.

[00:15] Let's create an array A. We'll equal it to a few values. We're going to assign a variable B to A. What this does is create a new variable with the same reference as A. We can see this by using a strictly equals equality check, because it will check the references are the same, not that the values are the same.

[00:37] Now, if I make an update to B by pushing a value onto it, if I log out A, we'll see that A has been updated as well. This is because A and B are not different arrays. They're a reference to the very same array.

[00:53] When we make changes to one, we actually make changes to the other. This can be problematic in our code. Functionality that's operating on B would change A, even if we didn't intend to do that. The same holds true for objects.

[01:07] If I create an object of A, we'll give a property foo, and we'll set it to bar. If I assign that reference to another variable B, and I make changes to B, such as reassigning foo to baz, if I log out A.foo, we'll see that it's been updated.

[01:27] This is problematic for functional programming, because it breaks the purity of our functions. When we make updates to data, we want to return brand new data structures that contain all the elements of the previous state of the data structure, plus our updates.

[01:43] For instance, push is a mutation on an array. As you saw, it changes all references to that same array. However, we could create an immutable push function. It'll receive a value and an array, and will return a brand new array by using the spread operator to create a clone, and then push the value onto that.

[02:08] If we create an array again of one, two, and three, this time, if I make a second array by pushing a value onto it using our new function -- we'll push four, and we'll use A -- we'll see that A hasn't been updated. We'll also see that A and B do not equal, because they are references to different arrays.

[02:32] We can create similar functionalities to handle changes to objects and so forth. One metaphor I like to use to describe this to people is the metaphor of taking a drink from a glass. I'm going to create two classes, a mutable glass and an immutable glass.

[02:50] We'll make a takeDrink method on both of them, and show the difference between handling it mutably and immutably. We'll start with the mutable glass. We'll have a constructor that takes a content and an amount, and assigns this.content to content and this.amount to amount.

[03:09] Then we'll create a takeDrink method that will take a value. What we will do is we'll update this.amount directly, and we'll return this instance of the glass. this.amount equals math.max. We're going to use this to guarantee that we never get less than zero amount in our instance, since we can't drink what's not there.

[03:32] Then we'll return this. If we create a glass, we'll call it MG1 equals new mutable glass. We'll give it a content of water and an amount of 100.

[03:45] Now, if we store the value returned to us after taking a drink, we can see that they are the same instance through a strictly equals comparison, and see that both MG1's amount and MG2's amount is the same, which makes sense, since they're the same instance.

[04:02] We'll log this out in our terminal, and we get true in each case. That's because we handle it mutably. We change the value directly, and we've changed the data structure after its creation. I'm going to comment these out, so that they don't turn up the next time I run the terminal.

[04:20] Now, we'll create an immutable glass. The constructor will look exactly the same, with a content and an amount. this.content equals content, this.amount equals amount. We'll make a takeDrink method that also takes a value, but here's where a big change occurs.

[04:37] When we take a drink, rather than returning back the same glass that I had before, I'm going to return a brand new glass with brand new content in the correct amount. To do this, we simply create a new immutable glass from within.

[04:53] We pass the content on again, because it should stay the same. We didn't turn water into wine or anything like that. Then for the amount, we'll do the same calculation, this.amount minus the value. The lowest it can be is zero, but because we return a whole new glass, we should see that when we create glasses, when we make updates, their instances are no longer the same.

[05:19] We'll create an IG1 variable. That's a new immutable glass. We'll also make it water, and we'll give it 100. Then if we store the glass returned to us after we take a drink from IG1 -- takeDrink, we'll do 20 -- what we should see is that when we compare IG1 and IG2, they aren't the same instance anymore.

[05:42] We can do that doing a strictly equals comparison. That should be false. We should see that the amount of IG1 hasn't changed, and thus does not equal the value in IG1. Both of these should be false.