A Guide to Variable Assignment and Mutation in JavaScript
Mutations are something you hear about fairly often in the world of JavaScript, but what exactly are they, and are they as evil as they’re made out to be?
In this article, we’re going to cover the concepts of variable assignment and mutation and see why — together — they can be a real pain for developers. We’ll look at how to manage them to avoid problems, how to use as few as possible, and how to keep your code predictable.
If you’d like to explore this topic in greater detail, or get up to speed with modern JavaScript, check out the first chapter of my new book Learn to Code with JavaScript for free.
Let’s start by going back to the very basics of value types …
Data Types
Every value in JavaScript is either a primitive value or an object. There are seven different primitive data types:
- numbers, such as
3
,0
,-4
,0.625
- strings, such as
'Hello'
,"World"
,`Hi`
,''
- Booleans,
true
andfalse
null
undefined
- symbols — a unique token that’s guaranteed never to clash with another symbol
BigInt
— for dealing with large integer values
Anything that isn’t a primitive value is an object, including arrays, dates, regular expressions and, of course, object literals. Functions are a special type of object. They are definitely objects, since they have properties and methods, but they’re also able to be called.
Variable Assignment
Variable assignment is one of the first things you learn in coding. For example, this is how we would assign the number 3
to the variable bears
:
const bears = 3;
A common metaphor for variables is one of boxes with labels that have values placed inside them. The example above would be portrayed as a box containing the label “bears” with the value of 3 placed inside.
An alternative way of thinking about what happens is as a reference, that maps the label bears
to the value of 3
:
If I assign the number 3
to another variable, it’s referencing the same value as bears:
let musketeers = 3;
The variables bears
and musketeers
both reference the same primitive value of 3. We can verify this using the strict equality operator, ===
:
bears === musketeers
<< true
The equality operator returns true
if both variables are referencing the same value.
Some gotchas when working with objects
The previous examples showed primitive values being assigned to variables. The same process is used when assigning objects:
const ghostbusters = { number: 4 };
This assignment means that the variable ghostbusters
references an object:
A big difference when assigning objects to variables, however, is that if you assign another object literal to another variable, it will reference a completely different object — even if both object literals look exactly the same! For example, the assignment below looks like the variable tmnt
(Teenage Mutant Ninja Turtles) references the same object as the variable ghostbusters
:
let tmnt = { number: 4 };
Even though the variables ghostbusters
and tmnt
look like they reference the same object, they actually both reference a completely different object, as we can see if we check with the strict equality operator:
ghostbusters === tmnt
<< false
Variable Reassignment
When the const
keyword was introduced in ES6, many people mistakenly believed that constants had been introduced to JavaScript, but this wasn’t the case. The name of this keyword is a little misleading.
Any variable declared with const
can’t be reassigned to another value. This goes for primitive values and objects. For example, the variable bears
was declared using const
in the previous section, so it can’t have another value assigned to it. If we try to assign the number 2 to the variable bears
, we get an error:
bears = 2;
<< TypeError: Attempted to assign to readonly property.
The reference to the number 3 is fixed and the bears
variable can’t be reassigned another value.
The same applies to objects. If we try to assign a different object to the variable ghostbusters
, we get the same error:
ghostbusters = {number: 5};
TypeError: Attempted to assign to readonly property.
Variable reassignment using let
When the keyword let
is used to declare a variable, it can be reassigned to reference a different value later on in our code. For example, we declared the variable musketeers
using let
, so we can change the value that musketeers
references. If D’Artagnan joined the Musketeers, their number would increase to 4:
musketeers = 4;
This can be done because let
was used to declare the variable. We can alter the value that musketeers
references as many times as we like.
The variable tmnt
was also declared using let
, so it can also be reassigned to reference another object (or a different type entirely if we like):
tmnt = {number: 5};
Note that the variable tmnt
now references a completely different object; we haven’t just changed the number
property to 5.
In summary, if you declare a variable using const
, its value can’t be reassigned and will always reference the same primitive value or object that it was originally assigned to. If you declare a variable using let
, its value can be reassigned as many times as required later in the program.
Using const
as often as possible is generally considered good practice, as it means that the value of variables remains constant and the code is more consistent and predictable, making it less prone to errors and bugs.
Variable Assignment by Reference
In native JavaScript, you can only assign values to variables. You can’t assign variables to reference another variable, even though it looks like you can. For example, the number of Stooges is the same as the number of Musketeers, so we can assign the variable stooges
to reference the same value as the variable musketeers
using the following:
const stooges = musketeers;
This looks like the variable stooges
is referencing the variable musketeers
, as shown in the diagram below:
However, this is impossible in native JavaScript: a variable can only reference an actual value; it can’t reference another variable. What actually happens when you make an assignment like this is that the variable on the left of the assignment will reference the value the variable on the right references, so the variable stooges
will reference the same value as the musketeers
variable, which is the number 3. Once this assignment has been made, the stooges
variable isn’t connected to the musketeers
variable at all.
This means that if D’Artagnan joins the Musketeers and we set the value of the musketeers
to 4, the value of stooges
will remain as 3. In fact, because we declared the stooges
variable using const
, we can’t set it to any new value; it will always be 3.
In summary: if you declare a variable using const
and set it to a primitive value, even via a reference to another variable, then its value can’t change. This is good for your code, as it means it will be more consistent and predictable.
Mutations
A value is said to be mutable if it can be changed. That’s all there is to it: a mutation is the act of changing the properties of a value.
All primitive value in JavaScript are immutable: you can’t change their properties — ever. For example, if we assign the string "cake"
to variable food
, we can see that we can’t change any of its properties:
const food = "cake";
If we try to change the first letter to “f”, it looks like it has changed:
food[0] = "f";
<< "f"
But if we take a look at the value of the variable, we see that nothing has actually changed:
food
<< "cake"
The same thing happens if we try to change the length property:
food.length = 10;
<< 10
Despite the return value implying that the length property has been changed, a quick check shows that it hasn’t:
food.length
<< 4
Note that this has nothing to do with declaring the variable using const
instead of let
. If we had used let
, we could set food
to reference another string, but we can’t change any of its properties. It’s impossible to change any properties of primitive data types because they’re immutable.
Mutability and objects in JavaScript
Conversely, all objects in JavaScript are mutable, which means that their properties can be changed, even if they’re declared using const
(remember let
and const
only control whether or not a variable can be reassigned and have nothing to do with mutability). For example, we can change the the first item of an array using the following code:
const food = ['🍏','🍌','🥕','🍩'];
food[0] = '🍎';
food
<< ['🍎','🍌','🥕','🍩']
Note that this change still occurred, despite the fact that we declared the variable food
using const
. This shows that using const
does not stop objects from being mutated.
We can also change the length property of an array, even if it has been declared using const
:
food.length = 2;
<< 2
food
<< ['🍎','🍌']
Copying by Reference
Remember that when we assign variables to object literals, the variables will reference completely different objects, even if they look the same:
const ghostbusters = {number: 4};
const tmnt = {number: 4};
But if we assign a variable fantastic4
to another variable, they will both reference the same object:
const fantastic4 = tmnt;
This assigns the variable fantastic4
to reference the same object that the variable tmnt
references, rather than a completely different object.
This is often referred to as copying by reference, because both variables are assigned to reference the same object.
This is important, because any mutations made to this object will be seen in both variables.
So, if Spider-Man joins The Fantastic Four, we might update the number
value in the object:
fantastic4.number = 5;
This is a mutation, because we’ve changed the number
property rather than setting fantastic4
to reference a new object.
This causes us a problem, because the number
property of tmnt
will also also change, possibly without us even realizing:
tmnt.number
<< 5
This is because both tmnt
and fantastic4
are referencing the same object, so any mutations that are made to either tmnt
or fantastic4
will affect both of them.
This highlights an important concept in JavaScript: when objects are copied by reference and subsequently mutated, the mutation will affect any other variables that reference that object. This can lead to unintended side effects and bugs that are difficult to track down.
The Spread Operator to the Rescue!
So how do you make a copy of an object without creating a reference to the original object? The answer is to use the spread operator!
The spread operator was introduced for arrays and strings in ES2015 and for objects in ES2018. It allows you to easily make a shallow copy of an object without creating a reference to the original object.
The example below shows how we could set the variable fantastic4
to reference a copy of the tmnt
object. This copy will be exactly the same as the tmnt
object, but fantastic4
will reference a completely new object. This is done by placing the name of the variable to be copied inside an object literal with the spread operator in front of it:
const tmnt = {number: 4};
const fantastic4 = {...tmnt};
What we’ve actually done here is assign the variable fantastic4
to a new object literal and then used the spread operator to copy all the enumerable properties of the object referenced by the tmnt
variable. Because these properties are values, they’re copied into the fantastic4
object by value, rather than by reference.
Now any changes that are made to either object won’t affect the other. For example, if we update the number
property of the fantastic4
variable to 5, it won’t affect the tmnt
variable:
fantastic4.number = 5;
fantastic4.number
<< 5
tmnt.number
<< 4
The spread operator also has a useful shortcut notation that can be used to make copies of an object and then make some changes to the new object in a single line of code.
For example, say we wanted to create an object to model the Teenage Mutant Ninja Turtles. We could create the first turtle object, and assign the variable leonardo
to it:
const leonardo = {
animal: 'turtle',
color: 'blue',
shell: true,
ninja: true,
weapon: 'katana'
}
The other turtles all have the same properties, except for the weapon
and color
properties, that are different for each turtle. It makes sense to make a copy of the object that leonardo
references, using the spread operator, and then change the weapon
and color
properties, like so:
const michaelangelo = {...leonardo};
michaelangelo.weapon = 'nunchuks';
michaelangelo.color = 'orange';
We can do this in one line by adding the properties we want to change after the reference to the spread object. Here’s the code to create new objects for the variables donatello
and raphael
:
const donatello = {...leonardo, weapon: 'bo staff', color: 'purpple'}
const raphael = {...leonardo, weapon: 'sai', color: 'purple'}
Note that using the spread operator in this way only makes a shallow copy of an object. To make a deep copy, you’d have to do this recursively, or use a library. Personally, I’d advise that you try to keep your objects as shallow as possible.
Are Mutations Bad?
In this article, we’ve covered the concepts of variable assignment and mutation and seen why — together — they can be a real pain for developers.
Mutations have a bad reputation, but they’re not necessarily bad in themselves. In fact, if you’re building a dynamic web app, it must change at some point. That’s literally the meaning of the word “dynamic”! This means that there will have to be some mutations somewhere in your code. Having said that, the fewer mutations there are, the more predictable your code will be, making it easier to maintain and less likely to develop any bugs.
A particularly toxic combination is copying by reference and mutations. This can lead to side effects and bugs that you don’t even realize have happened. If you mutate an object that’s referenced by another variable in your code, it can cause lots of problems that can be difficult to track down. The key is to try and minimize your use of mutations to the essential and keep track of which objects have been mutated.
In functional programming, a pure function is one that doesn’t cause any side effects, and mutations are one of the biggest causes of side effects.
A golden rule is to avoid copying any objects by reference. If you want to copy another object, use the spread operator and then make any mutations immediately after making the copy.
Next up, we’ll look into array mutations in JavaScript.
Don’t forget to check out my new book Learn to Code with JavaScript if you want to get up to speed with modern JavaScript. You can read the first chapter for free. And please reach out on Twitter if you have any questions or comments!