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:

  1. Inside of a function
  2. 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:

  1. function getValue(condition) {
  2. if (condition) {
  3. let value = "blue";
  4. // other code
  5. return value;
  6. } else {
  7. // value doesn't exist here
  8. return null;
  9. }
  10. // value doesn't exist here
  11. }

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:

  1. var count = 30;
  2. // Syntax error
  3. 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:

  1. var count = 30;
  2. // Does not throw an error
  3. if (condition) {
  4. let count = 40;
  5. // more code
  6. }

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:

  1. // Valid constant
  2. const maxItems = 30;
  3. // Syntax error: missing initialization
  4. 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:

  1. if (condition) {
  2. const maxItems = 5;
  3. // more code
  4. }
  5. // 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:

  1. var message = "Hello!";
  2. let age = 25;
  3. // Each of these would throw an error.
  4. const message = "Goodbye!";
  5. 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:

  1. const maxItems = 5;
  2. 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:

  1. const person = {
  2. name: "Nicholas"
  3. };
  4. // works
  5. person.name = "Greg";
  6. // throws an error
  7. person = {
  8. name: "Greg"
  9. };

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:

  1. if (condition) {
  2. console.log(typeof value); // ReferenceError!
  3. let value = "blue";
  4. }

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:

  1. console.log(typeof value); // "undefined"
  2. if (condition) {
  3. let value = "blue";
  4. }

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.