const is the new var
Published on 16 May 2020
• 4 min
Cette page est également disponible en français.
In this thirteenth installment of our daily series “19 nuggets of vanilla JS,” we broach a controversial topic: the respective roles of the three generic declarative keywords: var
, let
and const
(as there is no question about their two friends, function
and class
).
The series of 19
Check out surrounding posts from the series:
- Extracting emojis from a text
- Properly defining optional named parameters
const
is the newvar
(this post)- Using named captures
- Object spread vs.
Object.assign
- …and beyond! (fear not, all 19 are scheduled already)…
Before / after ES2015
Before ES2015, JavaScript only had two declarative keywords: var
and function
. Besides, the unit of scope wasn’t the block (as delimited by a pair of curly braces), unlike many other languages, but the function (or lacking one, the global scope).
This can catch you off-guard when coming from other languages; and it is never good for a programming language to behave in surprising ways: this is an infinite source of bugs.
As a result, ES2015’s three new declarative keywords (let
, const
and class
) all use the current block as their scope, which is way more aligned with the dominant paradigm in programming. Their identifiers only exist between the declaration line and the end of the block (even implicit blocks, like curly-less loops).
The hoisting of var
: an antipattern
It also happens that the two historical keywords are hoisted: this means that the JavaScript interpreter will act as if their declarations (but not their initializations, if any) happened at the top of the scope, regardless of their actual source position. To be more precise, var
declarations get hoisted first, then function
ones.
It was conceived as an attempt at preemptive optimization in the very first implementation of the engine, and doesn’t belong in 2020, or 2015, or even 2009. It is actually sneaky. Check this out:
function wtf() {
console.log(foo)
var foo = 42
console.log(foo)
}
wtf() // => logs 'undefined' then '42'
This is as counter-intuitive as it gets. By rights, that code shouldn’t even be syntactically valid. Failing that, it should at least throw a ReferenceError
on the first line of the function. But it doesn’t! Because of hoisting, the code that is actually run looks like this:
function wtf() {
var foo
console.log(foo)
foo = 42
console.log(foo)
}
There is zero benefit to hoisting variables. None. Zero. Zilch. In fact, the consensus today is that var
doesn’t belong in modern (ES2015+) JS code.
At a minimum, it should therefore be replaced by let
, that has two benefits:
- It is not hoisted, so any attempt at referencing it ahead of its declaration will throw a
ReferenceError
, as you would expect. - Its scope is the current block, which again aligns better with common expectations.
But then, why is this post about const
instead of let
?
Reassigment is a rare beast
There is exactly one difference between let
and const
: the latter doesn’t allow reassigning. A const
declaration must be initialized on-the-fly, and cannot be reassigned (e.g. with =
) later.
Why prefer const
?
In practice, the overwhelming majority of declarations are never reassigned; the reason is simple: altering the semantics of an identifier along the code creates confusion (reducing maintainability) and fosters bugs. Sometimes a reassignment is perfectly justified (e.g. async initialization, progressive refinement of a piece of data) but this remains a tiny minority case.
On the other hand, it often happens that we reassign by mistake. Why? Mostly due to sloppy copy-pastes or a clumsy code completion.
function oops() {
let superComplexStrut = 42
let superComplexStuff = bigCompute()
if (someCondition) {
superComplexStrut *= 2 // Oops, I meant "…Stuff"
}
// …
}
By going with const
by default, we’re immediately held back by the collar when trying to reassign an identifier by mistake. If ESLInt is properly configured (see later), it will spot this immediately (within seconds of typing the code, or at commit time with relevant hooks set up). At worst, we’ll get a runtime error (if our JS runtime understands const
anyway).
function oops() {
const superComplexStrut = 42
let superComplexStuff = bigCompute()
if (someCondition) {
// ESLint + TypeError: Assignment to constant variable.
superComplexStrut *= 2
}
// …
}
There is this tendency to stick with let
in numerical for
loops, but numerical for
loops should be super-rare now that we have the excellent for…of
loop (spoiler alert: a later nugget will cover this in-depth). No reassignment (such as i++
)? No need for let
!
// 🔴 BLEH
for (let i = 0, len = arr.length; i < len; i++) {
const item = arr[i]
// …
}
// ✅ Yeah!
for (const item of arr) {
// …
}
Beware of parameters
In a function, signature parameters are not declared with let
or const
, so stay sharp.
const
≠ immuable
Another critical point is: not reassignable does not mean immutable. In particular, if the identifier references an object, it’s still perfectly possible to alter the contents of it. You just can’t reassign the identifier.
const obj = { name: 'John' }
obj.name = 'Jane' // Perfectly possible
obj = { name: 'Jean-Gandalf' } // Perfectly impossible
If you wish to “freeze” an object, there are multiple levels you can go for; at top-level, the standard library ever since ES5 features Object.preventExtensions()
, Object.seal()
and Object.freeze()
, in increasing locking order. For recursive freezing, you’ll need to look for utility libraries such as deep-freeze-strict. But you can do it.
(On a side note, if you follow functional programming principles of immutability, you should not even need to do any of this.)
ESLint rules
Our beloved ESLint features several rules on this topic:
- no-var barks on any use of
var
- prefer-const spots any
let
orvar
declaration that is never reassigned and requires a switch toconst
- no-const-assign detects reassignments on a
const
-declared identifier; this is because, should your code be transpiled down to ES5, or run by an engine that doesn’t enforceconst
semantics (such as IE11’s), it would be runnable: the linter is your only safeguard then. This setting is part of the recommended preset, by the way: eslint:recommended.
Looking for other perspectives?
I said this is a controversial topic: you’ll find many people that think that const
should be reserved for “absolute” constants, not for processing steps in, say, a function’s body. They believe const
is not compatible with the concept of a variable, forgetting that in the vast majority of our codes, so-called variables… do not vary.
Still, it’s always a good idea to contrast several viewpoints when shaping your own opinion. The always relevant Dan Abramov wrote in 2019 on this topic, and I recommend you read what he had to say.
Want to dive deeper?
Our trainings are amazeballs, be they in-room or remote online, multi-client or in-house just for your company!