let vs const in JavaScript aka why don't you learn a bit of C?
Viktor Lázár
Posted on May 8, 2024
There's a raging war in the JavaScript community about when to use let
or const
and which one should not be used. Which is ridiculous. Some major JavaScript developers are blaming const
as it’s not useful because you can still mutate the value in an object referenced by a const
variable. Let’s get a deeper look on this and some background on from where we are coming from.
var
First, there was var
. Before ES6 you used var
only for variable declarations. But it was sometimes hard to understand what’s happening, because of hoisting. Hoisting means that basically all of your variable declarations are moved to the top of your scope, declaration position is ignored. So you can define your variable later in code and use your variable before declaration. Mind blowing, right? 🤯
x = 42;
console.log(x); // 42
var x;
The above example just works. Hoisting will move the var x;
to the top and that’s how you can use the x
variable anywhere in the scope. Although variable initialization is not working the same way, as only the variable declaration is hoisted, not the initialization. So the following will result in Nan
.
x = x + 1;
console.log(x); // NaN
var x = 0;
This behaviour is surely hard to understand cognitively and hard to maintain. So we moved over with ES6 to use let
and const
.
let
The let
keyword is easy to describe. You just need to forget about hoisting and it works the same way as var
. You can re-assign any value to the variable at any point.
x = 42; // ReferenceError: Cannot access 'x' before initialization
console.log(x);
let x;
Because of let
variable declarations are no longer hoisted, the above code will not work and will throw an error when you’re trying to use x
before initialization.
let x = 0;
x = x + 1;
console.log(x); // 1
Here we re-assign a new value to x
. Variable initialization set the value to x
to be 0
. Then we mutated the value of x
and so we got 1
as a result on the console. Let’s continue to const
!
const
ES6 also introduced the const
keyword for variable declaration. This already seems to be off, as a constant is not a variable. But the definition of “variable” explains some parts already:
"A variable in programming languages is a named storage location in computer memory that holds a data value. It is characterized by an identifier, which uniquely identifies it within the program. Variables can represent various types of data, such as integers, floating-point numbers, characters, or complex data structures. The value stored in a variable can change during the execution of the program, hence the term 'variable.' Programmers use variables to store and manipulate data, enabling dynamic behavior and flexibility in their programs.”
So variables are basically just identifiers to a slot in memory. When you use var
or let
the JavaScript runtime engine will let you change the value of that slot in memory. But when you use const
, you are instructing the JavaScript runtime engine to prevent any change of the variable, so you can’t update the memory identified by that variable.
const x = 0;
x = x + 1; // TypeError: Assignment to constant variable.
console.log(x);
You can only set the value to a const
variable at initialization time. const
variables are also not hoisted, the same way like let
variables.
The “problem” with const…
is that if your const
variable is a reference type, like an object, array or function, you can’t change the value of the variable, but you can mutate the value it’s referencing to.
const x = { a: 0 };
x.a = x.a + 1;
console.log(x.a); // 1
The above will perfectly work. But why? Is const
not preventing mutation? It is! But only for the x
variable. But why? Because x
is a reference only to the object! With other maybe familiar words: it’s a pointer!
If we go back in time to the dark ages of programming, when developers used the “C” programming language to create awesome apps (most games of the ‘90s were developed by using C and Assembly), we can understand what’s happening here.
#include <stdio.h>
int main() {
int num = 5;
const int *ptr = # // Declaring a constant pointer to an integer
printf("Initial value: %d\n", *ptr); // Output: Initial value: 5
// Attempting to modify the value through the const pointer
//*ptr = 10; // This line will result in a compilation error
// However, we can still modify the underlying value using a non-const pointer
int *mutablePtr = (int *)ptr;
*mutablePtr = 10;
printf("Modified value: %d\n", *ptr); // Output: Modified value: 10
return 0;
}
In C, you need to cast the const pointer to be able to change it, but as the const variable is only an identifier to a slot in memory, when you convince the compiler you are allowed to change the memory identified by the variable, well, you can do it! It’s just memory!
#include <stdio.h>
// Define a struct
struct Point {
int x;
int y;
};
int main() {
// Create an instance of struct Point
struct Point point = {5, 10};
// Declare a const pointer to a struct Point
const struct Point *ptr = &point;
printf("Initial values: (%d, %d)\n", ptr->x, ptr->y); // Output: Initial values: (5, 10)
// Attempting to modify the values through the const pointer
// ptr->x = 10; // This line will result in a compilation error
// However, we can still modify the underlying values using a non-const pointer
struct Point *mutablePtr = (struct Point *)ptr;
mutablePtr->x = 10;
mutablePtr->y = 20;
printf("Modified values: (%d, %d)\n", ptr->x, ptr->y); // Output: Modified values: (10, 20)
return 0;
}
This works for structs too, which is most close to what an object in JavaScript is. In JavaScript, your object represents a data structure in memory. As you are not specifying this object as non-mutable by using Object.freeze
, you can mutate the values held in that object. Keep in mind, that nested objects are basically just pointers too and Object.freeze
is only work for that single object you are freezing. For nested objects, you need to freeze every object used.
const x = Object.freeze({
a: {
b: 0
}
});
x.a.b = 42;
console.log(x.a.b); // 42
The above will just work, but when you change the code to this:
const x = Object.freeze({
a: Object.freeze({
b: 0
})
});
x.a.b = 42;
console.log(x.a.b); // 0
You will get back the same value as before, x.a.b
will be 0
. I admit, that this is still not the same as using a primitive const
variable as the above will not throw an error, it just silently doesn't change the value of x.a.b
.
So in case of a JavaScript const
variable, you can’t change the pointer / reference, but you change the underlying memory. It is a false assumption, that the const
specifier will affect the referenced object too. Why would it?
Cognitive load
What’s the role of let
when you want immutability all the time? Marking and flagging all involved parties that you want to change the value of the variable at some point in your code! This doesn’t mean that it has to be another developer, it can be a static analysis tool or AI, anyone or anything that can understand code.
During code reviews let
will help the reviewer to take special care about handling the variable. When I encounter such a case I instantly switch into questioning the authors code, why there’s a let
variable. Is there any reason to use let
? At any point is the value mutated? Why is it mutated?
Conclusion
As every language feature, let
and const
are just tools the developer can use. No one is forced to use const
. It is not necessary to do so. But it provides values and by not using it, you miss those values.
What’s the value in saving 2 characters? Time? Code completion and any type of artificial help in typing in the variable definition makes this obsolete. Size of the bundle used? 2kb, maybe 20k in case of a very large library? While we are watching billions of videos online?
It would be better to review over-engineered and unnecessary architectures or tech stacks we use, than complaining about using const
.
Posted on May 8, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 30, 2024