๐ถMutability
Last updated
Last updated
Mutability && Primitive && Reference Examples
In JavaScript, String
values are immutable, which means that they cannot be altered once created.
For example, the following code:
cannot change the value of myStr
to Job
, because the contents of myStr
cannot be altered. Note that this does not mean that myStr
cannot be changed, just that the individual characters of a string literal cannot be changed. The only way to change myStr
would be to assign it with a new string, like this:
Strings are passed by value, are immutable, and a new array is constructed and returned, because it cannot be changed in place.
Arrays
To dereference an array, use let [var1, var2]
syntax.
Objects
To dereference attributes from an object, use let {}
syntax.
References are everywhere in JS, but theyโre invisible. They just look like variables. Some languages, like C, call these things out explicitly as pointers, with their own syntax to boot. But JS doesnโt have pointers, at least not by that name. And JS doesnโt have any special syntax for them, either.
Take this line of JavaScript for example: it creates a variable called word
that stores the string โhelloโ.
Notice how word
points to the box with the โhelloโ. Thereโs a level of indirection here. The variable is not the box. The variable points to the box. Let that sink in while you continue reading.
Now letโs give this variable a new value using the assignment operator =
:
Whatโs actually happening here isnโt that the โhelloโ is being replaced by โworldโ โ itโs more like an entirely new box is created, and the word
is reassigned to point at the new box. (and at some point, the โhelloโ box is cleaned up by the garbage collector, since nothing is using it)
If youโve ever tried to assign a value to a function parameter, you probably realized this doesnโt change anything outside the function.
The reason this happens is because reassigning a function parameter will only affect the local variable, not the original one that was passed in. Hereโs an example:
Initially, only test
is pointing at the value โhelloโ.
Once weโre inside the function, though, both test
and word
are pointing at the same box.
After the assignment (word = "world"
), the word
variable points at its new value โworldโ. But we havenโt changed test
. The test
variable still points at its old value.
This is how assignment works in JavaScript. Reassigning a variable only changes that one variable. It doesnโt change any other variables that also pointed at that value. This is true whether the value is a string, boolean, number, object, array, functionโฆ every top-level variable works this way.
JavaScript has two broad categories of types, and they have different rules around assignment and referential equality. Letโs talk about those.
There are the primitive types like string, number, boolean (and also symbol, undefined, and null). These ones are immutable. a.k.a. read-only, canโt be changed.
When a variable holds one of these primitive types, you canโt modify the value itself. You can only reassign that variable to a new value.
The difference is subtle, but important!
Said another way, when the value inside a box is a string/number/boolean/symbol/undefined/null, you canโt change the value. You can only create new boxes.
It does not work like thisโฆ
This is why, for example, all of the methods on strings return a new string instead of modifying the string, and if you want that new value, youโve gotta store it somewhere.
The other category is the object type. This encompasses objects, arrays, functions, and other data stuctures like Map and Set. They are all objects.
The big difference from primitive types is that objects are mutable! You can change the value in the box.
If you pass a primitive value into a function, the original variable you passed in is guaranteed to be left alone. The function canโt modify whatโs inside it. You can rest assured that the variable will always be the same after calling a function โ any function.
But with objects and arrays (and the other object types), you donโt have that assurance. If you pass an object into a function, that function could change your object. If you pass an array, the function could add new items to it, or empty it out entirely.
So this is one reason why a lot of people in the JS community try to write code in an immutable way: itโs easier to figure out what the code does when youโre sure your variables wonโt change unexpectedly. If every function is written to be immutable by convention, you never need to wonder what will happen.
A function that doesnโt change its arguments, or anything outside of itself, is called a pure function. If it needs to change something in one of its arguments, itโll do that by returning a new value instead. This is more flexible, because it means the calling code gets to decide what to do with that new value.
Weโve talked about how assigning or reassigning a variable effectively โpoints it at a boxโ that contains a value. And how assigning a literal value (as opposed to a variable) creates a new box and points the variable at it.
This is true for primitive and object types, and itโs true whether itโs the first assignment or a reassignment.
Weโve talked about how primitive types are immutable. You canโt change them, you can only reassign the variable to something else.
Now letโs look at what happens when you modify a property on an object.
Weโll start with a book
object representing a book in a library that can be checked out. It has a title
and an author
and an isCheckedOut
flag.
Hereโs our object and its values as boxes:
And then letโs imagine we run this code:
Hereโs what that does to the object:
Notice how the book
variable never changes. It continues to point at the same box, holding the same object. Itโs only one of that objectโs properties that has changed.
Notice how this follows the same rules as earlier, too. The only difference is that the variables are now inside an object. Instead of a top-level isCheckedOut
variable, we access it as book.isCheckedOut
, but reassigning it works the exact same way.
The crucial thing to understand is that the object hasnโt changed. In fact, even if we made a โcopyโ of the book by saving it in another variable before modifying it, we still wouldnโt be making a new object.
The line let backup = book
will point the backup
variable at the existing book object. (itโs not actually a copy!)
Hereโs how that would play out:
The console.log
at the end further proves the point: book
is still equal to backup
, because they point at the same object, and because modifying a property on book
didnโt change the shell of the object, it only changed the internals.
Variables always point to boxes, never to other variables. When we assign backup = book
, JS immediately does the work to look up what book
points to, and points backup
to the same thing. It doesnโt point backup
to book
.
This is nice: it means that every variable is independent, and we donโt need to keep a sprawling map in our heads of which variables point to which other ones. That would be very hard to keep track of!
Wayyy back up in the intro I alluded to changing a variable inside a function, and how that sometimes โstays inside the functionโ and other times it leaks out into the calling code and beyond.
We already talked about how reassigning a variable inside a function will not leak out, as long as itโs a top-level variable like book
or house
and not a sub-property like book.isCheckedOut
or house.address.city
.
And anyway, this example used a string, so we couldnโt modify it even if we tried. (because strings are immutable, remember)
But what if we had a function that received an object as an argument? And then changed a property on it?
Hereโs what happens:
Look familiar? Itโs the same animation from earlier, because the end result is exactly the same! It doesnโt matter whether book.isCheckedOut = true
occurs inside a function or outside, because that assignment will modify the internals of the book
object either way.
If you want to prevent that from happening, you need to make a copy, and then change the copy.