Ever wondered why exactly we say that objects in JavaScript are “mutable”, but strings or numbers are not? Read on to discover a (not really) new way to think about the different types of values in JavaScript.
Table of contents
Open Table of contents
In the beginning there were Values…
In JavaScript, there are two main value types:
- Primitive values:
number
,string
,boolean
,null
,undefined
,BigInt
andSymbol
. - Objects: including object and array literals, functions,
Date
objects, regular expressions…
Immutable vs Mutable values
A value is mutable when it’s possible to change it. But, what does changing a value mean? Aren’t we changing values whenever we reassign a var
or let
variable?
To understand how immutability works, we need to reconsider how we think about variables and values.
Variables point to values
Let’s say we have the following code snippet:
let greeting = 'hello';
When someone starts learning JavaScript for the first time, usually they are told that variables are boxes that hold values:
However, it’s more accurate to think about variables pointing to values:
Primitive values are immutable. When we reassign a variable that points to an immutable value, we are not changing the value itself. If we do this:
let greeting = 'hello';
greeting = 'hi';
We are just changing to what value the variable is pointing to:
The old value remains unchanged. It’s not possible to update it (the value itself) in any way. If there aren’t any variables pointing to that value, and therefore it’s not being used, the JavaScript engine will eventually delete it in a process called garbage collection.
This means that the difference between let
and const
variables is not whether we can change their value, but whether we can reassign them: that is, whether we can change to what value they are pointing to.
Consider now this piece of code:
let greeting = 'hello';
let welcome = greeting;
Initially, both greeting
and welcome
point to the same value:
If we reassign the greeting
variable, will the value of welcome
also change?
let greeting = 'hello';
let welcome = greeting;
greeting = 'hi';
Not really! After reassigning the greeting
variable, it will point to the new value, but the welcome
variable still points to the original value:
Reference values
So now we’ve seen how primitive values work. What happens with objects, then?
At first, it may seem that they work the same as primitive values. Consider this code snippet:
let person = {
name: 'Jane',
lastName: 'Doe',
};
person = {
name: 'Sally',
lastName: 'Lockhart',
};
If we reassign the person
variable to a different object, the original object will be left alone, and the variable will just point to a new one:
But… what happens if we update a property of the object?
To understand it better, let’s change a bit our object representation:
All properties are like variables inside the object, but each property also points to a value, just like a regular variable does. So, if we update one of the properties:
let person = {
name: 'Jane',
lastName: 'Doe',
};
person.lastName = 'Lockhart';
This is what’s happening under the hood:
The object is still the same, but one of its properties has been mutated, and now points to a different value.
What’s the consequence of this behavior if we point two different variables to the same object?
Consider this piece of code:
let person = {
name: 'Jane',
lastName: 'Doe',
};
let personCopy = person;
As we’ve learned so far, both person
and personCopy
would point to the same object:
Can you guess what would happen if we updated the lastName
property on the personCopy
object?
let person = {
name: 'Jane',
lastName: 'Doe',
};
let personCopy = person;
personCopy.lastName = 'Lockhart';
console.log(personCopy.lastName); // ??
console.log(person.lastName); // ??
Let’s check out the visual representation of what happens under the hood:
First, the lastName
property stops pointing at the string value "Doe"
to start pointing to the string value "Lockhart"
. So far, so good. The issue is that objects are not primitive values: a variable that points to an object is actually pointing to a reference of that object. Since personCopy
points to the same object reference that person
does, it also updates person.lastName
! We have mutated the original object without realizing it.
If we wanted to avoid accidentally mutating our object, we would need to create a new object that has all the properties of the original one, for example using the spread operator:
let person = {
name: 'Jane',
lastName: 'Doe',
};
let personCopy = { ...person };
personCopy.lastName = 'Lockhart';
console.log(personCopy.lastName); // "Lockhart"
console.log(person.lastName); // "Doe"
Are there other implications of objects being reference values?
Consider now this piece of code:
let person1 = {
name: 'Jane',
lastName: 'Doe',
};
let person2 = {
name: 'Jane',
lastName: 'Doe',
};
console.log(person1 === person2); // ??
Let’s see what happens line by line. First, we create the person1
object:
Then, we create the person2
object:
This is the current picture of our code:
As we can see, person1
and person2
are pointing to two different object references. With all we’ve learned so far, what is going to get logged into the console?
let person1 = {
name: 'Jane',
lastName: 'Doe',
};
let person2 = {
name: 'Jane',
lastName: 'Doe',
};
console.log(person1 === person2); // false
person1
and person2
are not equal. Even though they point to objects that look exactly the same, each of them points to a different object reference, therefore they are two different values.
Recap
Let’s review the main ideas we’ve learned in this post:
- There are two types of values: primitive values and objects.
- Variables are not boxes that hold values, but rather point to them.
- Primitive values are immutable: they cannot be changed or destroyed. Every time we reassign a variable, we are not changing or destroying the original value, just pointing the variable to the new value.
- Objects are reference values: they point to a reference in memory. The object itself cannot be changed (just like any other value), but the properties inside of it can be mutated.
Bonus
If you feel like going deeper on the differences between primitive and reference values, check out these two blog posts:
- How primitive and reference values are allocated in memory
- JavaScript pass-by-value for primitive and reference values
That’s it for today! I hope you learned something new, and that you have now a better visual representation of how variables and values work under the hood :D