Block-Level Declarations
Block-level declarations are those that declare variables that are inaccessible outside of a given block scope. Block scopes, also called lexical scopes, are created:
- Inside of a function
- Inside of a block (indicated by the
{
and}
characters)
Block scoping is how many C-based languages work, and the introduction of block-level declarations in ECMAScript 6 is intended to bring that same flexibility (and uniformity) to JavaScript.
Let Declarations
The let
declaration syntax is the same as the syntax for var
. You can basically replace var
with let
to declare a variable, but limit the variable’s scope to only the current code block (there are a few other subtle differences discussed a bit later, as well). Since let
declarations are not hoisted to the top of the enclosing block, you may want to always place let
declarations first in the block, so that they are available to the entire block. Here’s an example:
function getValue(condition) {
if (condition) {
let value = "blue";
// other code
return value;
} else {
// value doesn't exist here
return null;
}
// value doesn't exist here
}
This version of the getValue
function behaves much closer to how you’d expect it to in other C-based languages. Since the variable value
is declared using let
instead of var
, the declaration isn’t hoisted to the top of the function definition, and the variable value
is no longer accessible once execution flows out of the if
block. If condition
evaluates to false, then value
is never declared or initialized.
No Redeclaration
If an identifier has already been defined in a scope, then using the identifier in a let
declaration inside that scope causes an error to be thrown. For example:
var count = 30;
// Syntax error
let count = 40;
In this example, count
is declared twice: once with var
and once with let
. Because let
will not redefine an identifier that already exists in the same scope, the let
declaration will throw an error. On the other hand, no error is thrown if a let
declaration creates a new variable with the same name as a variable in its containing scope, as demonstrated in the following code:
var count = 30;
// Does not throw an error
if (condition) {
let count = 40;
// more code
}
This let
declaration doesn’t throw an error because it creates a new variable called count
within the if
statement, instead of creating count
in the surrounding block. Inside the if
block, this new variable shadows the global count
, preventing access to it until execution leaves the block.
Constant Declarations
You can also define variables in ECMAScript 6 with the const
declaration syntax. Variables declared using const
are considered constants, meaning their values cannot be changed once set. For this reason, every const
variable must be initialized on declaration, as shown in this example:
// Valid constant
const maxItems = 30;
// Syntax error: missing initialization
const name;
The maxItems
variable is initialized, so its const
declaration should work without a problem. The name
variable, however, would cause a syntax error if you tried to run the program containing this code, because name
is not initialized.
Constants vs Let Declarations
Constants, like let
declarations, are block-level declarations. That means constants are no longer accessible once execution flows out of the block in which they were declared, and declarations are not hoisted, as demonstrated in this example:
if (condition) {
const maxItems = 5;
// more code
}
// maxItems isn't accessible here
In this code, the constant maxItems
is declared within an if
statement. Once the statement finishes executing, maxItems
is not accessible outside of that block.
In another similarity to let
, a const
declaration throws an error when made with an identifier for an already-defined variable in the same scope. It doesn’t matter if that variable was declared using var
(for global or function scope) or let
(for block scope). For example, consider this code:
var message = "Hello!";
let age = 25;
// Each of these would throw an error.
const message = "Goodbye!";
const age = 30;
The two const
declarations would be valid alone, but given the previous var
and let
declarations in this case, neither will work as intended.
Despite those similarities, there is one big difference between let
and const
to remember. Attempting to assign a const
to a previously defined constant will throw an error, in both strict and non-strict modes:
const maxItems = 5;
maxItems = 6; // throws error
Much like constants in other languages, the maxItems
variable can’t be assigned a new value later on. However, unlike constants in other languages, the value a constant holds may be modified if it is an object.
Declaring Objects with Const
A const
declaration prevents modification of the binding and not of the value itself. That means const
declarations for objects do not prevent modification of those objects. For example:
const person = {
name: "Nicholas"
};
// works
person.name = "Greg";
// throws an error
person = {
name: "Greg"
};
Here, the binding person
is created with an initial value of an object with one property. It’s possible to change person.name
without causing an error because this changes what person
contains and doesn’t change the value that person
is bound to. When this code attempts to assign a value to person
(thus attempting to change the binding), an error will be thrown. This subtlety in how const
works with objects is easy to misunderstand. Just remember: const
prevents modification of the binding, not modification of the bound value.
The Temporal Dead Zone
A variable declared with either let
or const
cannot be accessed until after the declaration. Attempting to do so results in a reference error, even when using normally safe operations such as the typeof
operation in this example:
if (condition) {
console.log(typeof value); // ReferenceError!
let value = "blue";
}
Here, the variable value
is defined and initialized using let
, but that statement is never executed because the previous line throws an error. The issue is that value
exists in what the JavaScript community has dubbed the temporal dead zone (TDZ). The TDZ is never named explicitly in the ECMAScript specification, but the term is often used to describe why let
and const
declarations are not accessible before their declaration. This section covers some subtleties of declaration placement that the TDZ causes, and although the examples shown all use let
, note that the same information applies to const
.
When a JavaScript engine looks through an upcoming block and finds a variable declaration, it either hoists the declaration to the top of the function or global scope (for var
) or places the declaration in the TDZ (for let
and const
). Any attempt to access a variable in the TDZ results in a runtime error. That variable is only removed from the TDZ, and therefore safe to use, once execution flows to the variable declaration.
This is true anytime you attempt to use a variable declared with let
or const
before it’s been defined. As the previous example demonstrated, this even applies to the normally safe typeof
operator. You can, however, use typeof
on a variable outside of the block where that variable is declared, though it may not give the results you’re after. Consider this code:
console.log(typeof value); // "undefined"
if (condition) {
let value = "blue";
}
The variable value
isn’t in the TDZ when the typeof
operation executes because it occurs outside of the block in which value
is declared. That means there is no value
binding, and typeof
simply returns "undefined"
.
The TDZ is just one unique aspect of block bindings. Another unique aspect has to do with their use inside of loops.