If you've already read some of the sibling posts, you can skip straight to here.
- The basics: declaring variables
- What is it?
- Okay...but what does it do?
- What is it good for?
- When should I use something else?
- So when should I use it?
Let's begin at the beginning: variable declarations declare variables. This may seem obvious to many, but in practice we often confuse variables with values, and it is important, particularly for this conversation, that we are clear on the differences.
A variable is a binding between a name and a value. It's just a box, not the contents of the box, and the contents of the box may vary either in part or in whole (hence the term 'variable').
The kind of box you use, that is, the declarator you use to create a binding, defines the way it can be handled by your program. And so when it comes to the question of, "How should I declare my variables?" you can think of the answer in terms of finding a box for your data that is best suited to the way you need to manipulate it.
functionkeyword is not, in fact, a variable declarator, but it can create a bound identifier. I won't focus on it here; it's a bit special because it can only bind identifiers to values of a particular type (function objects), and so has a rather undisputed usage.
Why so many options? Well, the simple answer is that in the beginning, there was only
var; but languages evolve, churn happens, and features come (but rarely go).
One of the most useful features in recent years was the addition of block scoping to the ECMAScript 2015 Language specification (a.k.a. ES6), and with it came new tools for working with the new type of scope.
In this post, we'll dive into the behavior of one of these new block-scope tools:
But it would be rather useless without the ability to declare variables which exist only within these 'blocks' of scope.
constdeclarations define variables that are scoped to the running execution context's LexicalEnvironment. The variables are created when their containing Lexical Environment is instantiated but may not be accessed in any way until the variable's LexicalBinding is evaluated. A variable defined by a LexicalBinding with an Initializer is assigned the value of its Initializer's AssignmentExpression when the LexicalBinding is evaluated, not when the variable is created.
...creates a new immutable binding for the name N that is uninitialized. A binding must not already exist in this Environment Record for N....
It is a Syntax Error if Initializer is not present and IsConstantDeclaration of the LexicalDeclaration containing this LexicalBinding is true.
Let defaultValue be the result of evaluating Initializer.
Translation? 🤨 Let's learn by doing.
const binds a name to a value and doesn't let me bind it to anything else.
During compilation, that variable is
- scoped to the nearest enclosing lexical environment (i.e. a block, a function, or the global object) and
- created as immutable but not initialized during the instantiation of that scope
💡 You might come across the term "variable hoisting" in your JS travels: it refers to this 'lifting' behavior of the JS compiler with respect to creating variables during instantiation of the scope itself. All declarators hoist their variables, but the accessibility of those variables varies with the tool you use.
At run-time, my variable is initialized and references to it can then be evaluated and manipulated.
undefined😓 The term "not defined," in this context, should really be "not declared."
A run-time reference to a variable declared with
const is not valid unless it occurs after the variable declaration, with respect to the current flow of execution, not necessarily the "physical" location of the declaration in my code. For example, this is valid:
But this will give me a run-time error:
Furthermore, additional declarations of the same name in the same scope using
let are not allowed: the name is essentially reserved by the first declaration encountered by the compiler.
const restricts access to my box to the to the nearest enclosing lexical environment, not merely the closest function, and so
const really shines at close-quarters data management.
var, my box is initialized with a value and can never be re-assigned, making it a great tool to employ in an immutable approach to state management. The string "const" in English is highly associated with the word constant, and so
And since functions inherit the environment of their parents thanks to closure, a function nested within such a block can access the
let) bindings of their parent scopes, but not vice-versa.
Sometimes, I want a box that will hold different things as my program executes, like as a counter or a flag.
const forbids re-assignments, and so it will not work for this use case. I must use
Sometimes, I need to manage state that is accessible across an entire function of decent size, not just a short block of code. Since
const scopes my data to the nearest lexical environment, it will work for this purpose, but it communicates the wrong thing to my readers and so it's not the best tool for this job. In this situation,
var is better.
Sometimes, I want a name that always means exactly one thing, but whose bound value may evolve throughout my program. Since
const prevents re-assignments but doesn't care about changes to inherently mutable values, it will work for this purpose, but it communicates the wrong thing to my readers.
Something that changes is not constant, and the strong association of
const to the word constant makes it misleading in this context. For this situation, I prefer employing
let in combination with SCREAMING_SNAKE_CASE to communicate to readers that I intend the meaning to remain constant, but the exact value may vary.
const inappropriately can hurt the readability and maintainability of my code because I'm communicating the wrong thing and not encapsulating my data as well as I could be.
To learn how to communicate better in my code, I dove into the other tools available and wrote about what I found:
The reason to use
const isn't to declare constant values, it's to declare constant bindings.
In these situations I prefer using
const in combination with SCREAMING_SNAKE_CASE for communicating, "This is a constant, and will never change in meaning or value over the course of this block." I find the association with the word constant overpowers everything else about
const, and so I don't tend to use it for any other purpose.
The block could be something like an
for loop, or even an anonymous block; one of the main values of
const is in keeping variables close to where they are used without exposing them to the wider world of the enclosing function.
If a function definition is particularly short, say only two or three lines long, and my other criteria hold true, I may prefer to use a
const, but the value of such a short-lived
const is highly context-specific. In this case the value of
let is entirely in what it communicates to my readers: this variable is short-lived and it never changes, you can forget about it soon and be at peace 😌.
Every tool has its use. Some can make your code clearer to humans or clearer to machines, and some can strike a bit of balance between both.
"Good enough to work" should not be "good enough for you." Hold yourself to a higher standard: learn a little about a lot and a lot about a little, so that when the time comes to do something, you've got a fair idea of how to do it well.