Let It Be - How to declare JavaScript variables
Wow! This takes me back! Please check the date this post was authored, as it may no longer be relevant in a modern context.
ES2015 added a variety of riches to JavaScript. Among them are two new ways
to declare variables, let
and const
. These tools were introduced to
address faults of var
, provide us an opt-in path to new functionality, and
bring richers semantics to JavaScript variable declarations.
Let’s talk about const
, let
, and how we declare variables in JavaScript.
Why let
?
Before ES2015 there was a single way to declare variables in JavaScript. This
one tool was var
. Since JavaScript is additive (changing the meaning of
var
would break some number of websites) developers can still use var
.
There are some problems with the way var
works that needed to be fixed.
Variables declared with var
are scoped to a function, not to a block.
For example this is valid use of var
:
// In strict mode
function hello(place) {
if (place === 'world') {
var string = 'SO CREATIVE OF YOU';
}
return string || `hello ${place}`;
}
hello('friend'); // => "hello friend"
hello('world'); // => "SO CREATIVE OF YOU"
In this example
string
is “hoisted”. It is declared at the function’s scope (with a default value ofundefined
).string
is scoped to the function. When assigned a value, that value remains for the whole function.
By comparison let
variables are only scoped to their block.
For example:
// In strict mode
function hello(place) {
if (place === 'world') {
let string = 'SO CREATIVE OF YOU';
}
return string || `hello ${place}`;
}
hello('friend'); // => throws ReferenceError: string is not defined
hello('world'); // => throws ReferenceError: string is not defined
In the case of “friend”, string
is not declared. Its use after return
is an invalid reference. In the case of “world”, string
is declared and provided a value, however that declaration and value pass out of scope after the following }
.
var
is commonly mistaken for having block scope. For example, with for
:
for (var i = 0; i < 10; i++) {
// Some logic
}
// i has leaked out of the for loop's { } block.
console.log(i); // => logs "10"
By producing code with shorter variable lifetimes by default, let
helps developers
avoid common foot-guns. When long-lived variables are used, they are intentional
and easy to identify.
Second, var
declared variables can de re-declared. For example:
// In strict mode
function hello(place) {
var string = `hello ${place}`;
// Some logic...
// Declare string, again
var string = 'SO CREATIVE OF YOU';
return string;
}
hello('friend'); // => "SO CREATIVE OF YOU"
If you found the above code in a program, you would presume the author did not
intend to over-write the initial declaration of string
. Code like this is
almost certainly in error.
let
will throw an exception when a variable is re-declared in the same scope.
For example:
// In strict mode
function hello(place) {
let string = `hello ${place}`;
// Some logic...
// Declare string, again
let string = 'SO CREATIVE OF YOU';
return string;
}
// Throws on compilation with SyntaxError: Identifier 'string' has already been declared
This improvement eliminates the silent failure of variable naming collisions. Instead the developer is notified before the script executes that there is an error.
Finally, let
can be used with a new feature of the language: Destructuring. For
example:
node.addEventListener('click', function(event) {
let { target } = event;
jQuery(target).addClass('clicked');
}
Destructuring is one of my favorite language features arriving in JavaScript,
and you can only get it with let
, const
and with var
. Learn more about destructuring
in this great blog post at 2ality.com.
Why const
?
Constants are a feature of many languages. A constant variable is assigned a value once at declaration and rejects any attempts to change the value. An example of constant usage in Ruby:
A_CONST = 'foo';
A_CONST = 'baz'; # warning: already initialized constant FOO
This kind of variable is useful in JavaScript as well. To create constants in
JavaScript, developers use the const
keyword:
// parity.js
export const EVEN = Symbol('even');
export const ODD = Symbol('odd');
function parity(integer) {
return integer % 2 ? ODD : EVEN;
}
export default parity;
import { ODD, EVEN }, parity from 'parity';
let number = 23;
switch (parity(number)) {
case ODD:
console.log(`How odd number ${number}!`);
break;
case EVEN:
console.log(`Even-Steven ${number}`);
break;
default:
throw new Error("That ain't good");
}
Two goals are met using const
and CAPS
in the above code:
- The variables cannot be given a new value. Setting a
const
would raise an exception. - The code, by using
const
andCAPS
, hints to the reader that changing these values would break the program.
Note that const
variables are protected from re-assignment, but are still
mutable. This is a common point of confusion. For example:
const NAME = 'Bob Marley';
// Attempting to re-assign a const raises an exception
NAME = 'Rick Astley'; // throws TypeError: Assignment to constant variable.
const FULL_NAME = {
first: 'Bob',
last: 'Marley'
};
// Re-assigning any value, including an object raises an exception
FULL_NAME = {
first: 'Rick',
last: 'Astley'
}; // throws TypeError: Assignment to constant variable.
// Modifiying properties on an object is just fine though
FULL_NAME.first = 'Rick';
FULL_NAME.first; // => 'Rick';
FULL_NAME
cannot have a new value assigned, but properties on the object that
is its value can be assigned new values. Changing properties
of an object or members of an array assigned to a const
is permitted.
A const
variable cannot be re-assigned, but the value of a const
is still mutable.
The semantic justification for const
is driven home by this limitation.
It can sometimes makes sense to use const
even if the value object remains
mutable. The lexical meaning of const
is more important its behavior.
An example:
const SAMPLE_RATES = {
LOW: 'Nick',
HIGH: 'Nancy'
}
This code does not protect SAMPLE_RATES.LOW
from being assigned a new value,
however it would certainly be bad form to do so. const
has semantic value
that expresses the author did not intend these values to change. It suggests
to future travelers that they should tread lightly.
Liberal let
Both let
and const
are great additions to the language. However they’ve
also introduced some new complexity.
When declaring a variable, should you use var
, let
, or const
?
If you can, do not use var
. The problems that exist with it are
real, and you will write better code without it.
So what are the guidelines for choosing between let
and const
?
For most variables, use let
. For constants, use const
.
Constants should be declared at the top of modules and only in module scope.
Some things that should be declared with const
are:
- Configuration values that are defined once, such as
const TEMPLATE = 'Hi %s!'
- Most exported variables. For example
export const ODD = Symbol('odd')
- Bespoke classes, like
const MyInputComponent = Ember.Component.extend()
- Any values imported to a module are already constants, and will raise an exception if you attempt to re-assign them
Some things that should be declared with let
:
- Most variables, especially any that are not at the module level. If it is
inside a function or inside a block, use
let
- Any values that are function arguments already behave like
let
declarations, though you may not want to re-assign them for performance reasons (browser JITs don’t seem to like it).
This example uses let
and const
in an ideal manner:
// Constants and imports are grouped together, and use CAPS
import PLACEHOLDER from 'placeholder';
const TEMPLATE = `Howdy ${PLACEHOLDER}!`;
const SPECIAL_GUEST = 'Warren';
// Variables in `welcome` use `let`, regardless of if they change
function welcome(name) {
let message = TEMPLATE.replace(PLACEHOLDER, name);
if (name === SPECIAL_GUEST) {
message += ' You old so and so';
}
let wordCount = message.split(' ').length;
return {message, wordCount};
}
welcome('Rich'); // => { message: 'Howdy Rich!', wordCount: 2 }
Constantly const
There is another school of thought on when to use let
and const
I’ll
need to address. This strategy suggests developers use const
as much as
possible. Any variable that is not re-assigned should be declared with
const
. For example:
// Re-assigned variables use `let`, if they are assigned once use `const`
function welcome(name) {
let message = TEMPLATE.replace(PLACEHOLDER, name);
if (name === SPECIAL_GUEST) {
message += ' You old so and so';
}
const wordCount = message.split(' ').length;
return {message, wordCount};
}
I think this usage is poor practice. It adds an extra distraction to the process of programming, and results in code difficult to understand and change.
First, aggressive use of const
devalues the operator. Take this example:
import PLACEHOLDER from 'placeholder';
const TEMPLATE = `Howdy ${PLACEHOLDER}!`;
function welcome(name) {
const message = TEMPLATE.replace(PLACEHOLDER, name);
const wordCount = message.split(' ').length;
return {message, wordCount};
}
Why is message
a const
? “Constantly const
” say it should be so
because it doesn’t change. So what if we add a feature to this code:
import PLACEHOLDER from 'placeholder';
const TEMPLATE = `Howdy ${PLACEHOLDER}!`;
const SPECIAL_GUEST = 'Warren';
function welcome(name) {
const message = TEMPLATE.replace(PLACEHOLDER, name);
if (name === SPECIAL_GUEST) {
// Wait! You can't change a `const` like this!
message += ' You old so and so';
}
const wordCount = message.split(' ').length;
return {message, wordCount};
}
The suggestion is that now, when adding a re-assignment, the developer should
go back and change the message
declaration to let
. What was the value
of making it a constant then? In comparison, TEMPLATE
and SPECIAL_GUEST
are true constants. A developer knows that they should think twice before
changing their declaration.
We showed above with SAMPLE_RATES.LOW
that the semantics of const
are
important. Even though LOW
can be changed, const
hints to the reader
of the author’s intent. The semantics of const
are more important than
the behavior.
Constantly const
advocates suggest there is “expressiveness” in having all
variables with stable values use const
. There are two holes in this logic:
- By
const
being the default declaration,let
rises as the more visible style of declaration. The idea is thatlet
flags where something funny is happening. However, function arguments likefunction(a,b,c) {
are also allowed re-assignment, so it is a false sense of security to suggest nolet
means no funny business is happening. - What is “expressed” by
const
itself when used this way? Since you are intended to refactor the declaration tolet
if the situation requires it, it can only express “this variable wasn’t being re-assigned when I wrote this code, but feel free to change that”. This is basically meaningless. In comparison, the expression ofconst
in liberallet
it akin to “this should not be re-assigned, proceed with caution!” which is far more useful.
Some adherents of this style even suggest developers should use const
but
change to let
only after their build tooling complains! To opt into such a
slow process for little or no benefit is just short of madness.
Second, choosing const
first really means choosing to think about every
declaration. Will the next line of code change this assignment? How will I
use this variable? Choosing let
first eliminates this intellectual load,
helping developers focus on more important problems.
Given that you write a program like the following (declarations as ____
):
function sortClowns(clowns) {
____ length = clowns.length;
for (____ i=0; i<length; i++) {
- When declaring
length
, a developer is already thinking about if the algorithm involves decrementing that number to zero or not. You might not think you considered it, but your brain did. - When declaring
i
, it is unreasonable to suggest any developer would declare it withconst
first, just to typei++
a few characters later then go back to make the declarationlet
. This kind of second-guessing happens all the time when developers chooseconst
-first.
A common third argument in favor of constantly const
is the incorrect perception
that it may improve performance. The arguments goes: Because browers know a
variable will not change, they can better optimize… something. There is no known
performance difference between let
and const
. Despite any intuition we,
as web developers, may have for what a JIT should and should not optimize,
there is no justification for const
on performance grounds. The performance
rational is mostly based on confusion with the benefits of immutable data
structures.
Because it is more distracting to write, and decreases the explicit semantics
of const
, I do not suggest you write code const
-first. Stick with liberal
let
, and focus on solving actual programming problems.
Automate, Automate, Automate
I feel pretty strongly that liberal let
is a better strategy than constantly
const
, but there is one more suggestion I want to drive home.
Whatever style of declaration you adopt, automate the enforcement of idiomatic usage in your codebase. Nothing wastes developer time more than going back to a PR because they haven’t memorized the style guide.
There are a number of tools that can help with this:
- There is a jshint option to disallow
var
. - JSCS also has a rule to disallow
var
. - ESLint has a rule to disallow
var
andprefer-const
for constantlyconst
followers.
Use automated style-guide enforcement to ensure, no matter how you declare variables, that your codebase is consistent.