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
stringis “hoisted”. It is declared at the function’s scope (with a default value ofundefined).stringis 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
constwould raise an exception. - The code, by using
constandCAPS, 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
letdeclarations, 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
constbeing the default declaration,letrises as the more visible style of declaration. The idea is thatletflags 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 noletmeans no funny business is happening. - What is “expressed” by
constitself when used this way? Since you are intended to refactor the declaration toletif 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 ofconstin liberalletit 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 withconstfirst, 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
varandprefer-constfor constantlyconstfollowers.
Use automated style-guide enforcement to ensure, no matter how you declare variables, that your codebase is consistent.