In contrast to other curly brace programming languages like C, C++, Java, or C#, JavaScript shows some unusual behavior. Take a look at this code snippet:
// Exercise-01-01.js
function getWiggin(childIndex) {
if (childIndex === 1) {
var wiggin = 'Peter';
}
else if (childIndex === 2) {
var wiggin = 'Valentine';
}
else if (childIndex === 3) {
wiggin = 'Ender';
}
return wiggin;
}
console.log("The First:", getWiggin(1));
console.log("The Second:", getWiggin(2));
console.log("The Third:", getWiggin(3));
console.log("Unknown:", getWiggin(4));
The multiple declarations of the wiggin
variable in getWiggin
suggests that the instance within the childIndex === 1
branch is different from the other created in the childIndex === 2
section. Your instinct whispers that both of them are visible only within their hosting blocks. Nonetheless, the function uses a single instance of wiggin
, as the code output shows:
The First: Peter
The Second: Valentine
The Third: Ender
Unknown: undefined
Variable Hoisting
You are deceived by a JavaScript feature, variable hoisting. While the engine parses the code, it hoists the wiggin
variable declaration to the top of the getWiggin
function as if you wrote this:
// Exercise-01-02.js
function getWiggin(childIndex) {
var wiggin;
if (childIndex === 1) {
wiggin = 'Peter';
}
else if (childIndex === 2) {
wiggin = 'Valentine';
}
else if (childIndex === 3) {
wiggin = 'Ender';
}
return wiggin;
}
// ...
Global Variables
Variables declared in the global scope may cause surprises, too. Since a var
declaration in the global scope creates a variable, which is a property on the global object—the windows
object in browsers—, you can override an existing global. This action may lead to unexpected behavior. Take a look at this code:
// Exercise-01-03.js
var Buffer = "Battle School";
console.log(window.Buffer);
The declaration of Buffer
is in the global scope. Thus, assigning a string value to this variable overrides the window.Buffer
property. You may not know that Buffer
is a predefined function in the global scope. When the code runs in the browser, the second line of the console output—the browser writes this log item and not the exercise code—clearly indicates that redefining Buffer
has unexpected side effects:
Battle School
Uncaught TypeError: e.Buffer.isBuffer is not a function
For-Loop Variables
Although seasoned developers know that a for-loop variable’s scope is not constrained to the body of the loop, novices are not necessarily aware of this fact:
// Exercise-01-04.js
var wiggins = ['Peter', 'Valentine', 'Ender'];
for (var i = 0; i < wiggins.length; i++) {
console.log(i, wiggins[i]);
}
console.log('The loop variable is still valid:', i);
The last line of the code output points out that the i variable still holds its value, though the execution flow has left the block inside the for-loop:
0 'Peter'
1 'Valentine'
2 'Ender'
The loop variable is still valid: 3
This behavior cannot cause such issues as the accidental override of a global variable, but still, instinctively, you might find it controversial.
Block-Level Declarations
Many curly-brace languages support block scopes, and so does ECMAScript 2015. You can use the let
declaration with the same syntax as var
, but in contrast to var
, let
limits the particular variable’s scope to the current code block. Rewriting the getWiggin
function with let
declarations would close the wiggin
variable into code blocks:
// Exercise-01-05.js
function getWiggin(childIndex) {
if (childIndex === 1) {
let wiggin = 'Peter';
}
else if (childIndex === 2) {
let wiggin = 'Valentine';
}
else if (childIndex === 3) {
let wiggin = 'Ender';
}
return wiggin;
}
console.log("The First:", getWiggin(1));
console.log("The Second:", getWiggin(2));
console.log("The Third:", getWiggin(3));
console.log("Unknown:", getWiggin(4));
As you run this code, the first call of getWiggin
raises a ReferenceError
instance telling you that wiggin
is not defined. The three let
declarations create three separate variables, each visible only within its hosting block—between the opening and closing curly braces of the corresponding if
. The return wiggin
statement references to an undeclared variable—none of the previously used instances of wiggin is visible at that point—, and this is why the JavaScript engine raises an error.
The let Declaration
The following code uses the commander
variable in three different var
declaration:
// Exercise-01-06.js
var useSpare = true;
var commander = 'Ender Wiggin';
if (useSpare) {
var commander = 'Bean';
}
console.log(commander);
var commander = 'Petra Arcanian';
The var
keyword allows you to redeclare the same variable multiple times. Due to variable hoisting, you have only one commander
variable here, which is valid within the entire context of this sample code. This little JavaScript snippet outputs “Bean”.
With let
, you can hide a variable in an outer block. Should you change the first two occurrences of var
in the previous code snippet to let
, the output would display “Ender Wiggin”:
// Exercise-01-07.js
var useSpare = true;
let commander = 'Ender Wiggin';
if (useSpare) {
let commander = 'Bean';
}
console.log(commander);
// var commander = 'Petra Arcanian';
Although the execution flow goes into the if
block, the commander
variable declared there is a separate one from other that sets its value to “Ender Wiggin”. While in the inner block, the second declaration hides commander
in the outer block. As soon as the execution flow leaves the if
block, the inner commander
goes out of the scope. It does not hide the outer variable anymore, and thus we get a different output.
The let
declaration prevents you from redefining the same variable in the same scope.
Observe that the third commander
declaration in the last line is commented out. Should you uncomment this line, you would get a SyntaxError
with a message of “Identifier ‘commander’ has already been declared”.
The let
declaration mends the for-loop variable issue you saw in Exercise-01-04.js
. As this code sample demonstrates, the i
loop variable is valid only within the loop’s body:
// Exercise-01-08.js
var wiggins = ['Peter', 'Valentine', 'Ender'];
for (let i = 0; i < wiggins.length; i++) {
console.log(i, wiggins[i]);
}
// Raises a ReferenceError: 'i is not defined':
console.log('The loop variable is not valid:', i);
No Hoisting with let
While the var
declaration hoists the variable to the top of its declaring context—global or function—, let
does not. The following code snippet raises an error (ReferenceError
) with the typeof child
condition, because, at the point where the expression is used, the child
variable is not defined yet:
// Exercise-01-09.js
var wiggins = ['Peter', 'Valentine', 'Ender'];
for (let i = 0; i < wiggins.length; i++) {
console.log(typeof child);
let child = wiggins[i];
}
When the JavaScript engine finds a block, it scans it for variable declarations before processing the statements within the block. If the engine finds a var
declaration, it hoists the variable to the top of the corresponding context (function or global scope). When it finds a let
(or const
) declaration, it moves the variable into a temporal store—often mentioned as Temporal Dead Zone, or TDZ. While processing the block statements, any access attempt to a variable in TDZ raises a ReferenceError. When the engine reaches the let
or const
declaration, it removes the variable from TDZ, and thus excepts the subsequent references to the variable.
The const Declaration
In tandem with let
, ES 2015 introduces another declaration, const
. Its syntax is similar to let
. The JavaScript engine takes the variables declared with const
into account as constants, and thus it does not allow assigning a new value to them:
// Exercise-01-10.js
const army = "Dragon";
console.log("Ender's army: ", army);
army = "Salamander";
Because const
forbids reassignment, the last code line raises a TypeError
with the “Assignment to constant variable” message.
Note: When you define a const, you need to initialize it immediately. If you omit the initialization, the engine raises a SyntaxError
.
Just as let
, const
is a block-level declaration:
// Exercise-01-11.js
var battleIsComing = true;
if (battleIsComing) {
const commander = 'Ender';
console.log(commander, 'leads us');
}
console.log(commander);
The last line of this code raises a ReferenceError
because commander
is not visible outside its declaring block, the true condition branch of if
.
Using const with Objects
The const
declaration does not prevent you from changing the properties of an object that is assigned to a const
variable:
// Exercise-01-12.js
const petraStats = {
name: 'Petra',
wins: 12,
rank: 4
};
petraStats.wins = 14;
petraStats.rank = 3;
console.log(petraStats);
As the code shows, you can assign new values to the properties of petraStats
. JavaScript assigns object references to variables. Because the reference to petraStats
does not change when you set the wins
and rank
properties of the object, this construct is entirely valid.
However, a new object assignment violates the const
rule, and so the second assignment in this code raises a TypeError
:
// Exercise-01-13.js
const petraStats = {
name: 'Petra',
wins: 12,
rank: 4
};
petraStats = {
name: 'Petra',
wins: 14,
rank: 3
};
console.log(petraStats);
Using const in Loops
When you apply const
in loops, there are a few things you should be aware of. The following code snippet—in which the child
variable is set in each iteration of the for-loop—is valid:
// Exercise-01-14.js
var wiggins = ['Peter', 'Valentine', 'Ender'];
for (let i = 0; i < wiggins.length; i++) {
const child = wiggins[i];
console.log(child + ' is a Wiggin');
}
Because the lifetime of child
is bound to a single iteration and its initial value is set only once in each iteration, this construct works as you expect.
You may think that you can change the let
declaration of i
to const
, but this approach does not work:
// Exercise-01-12.js
var wiggins = ['Peter', 'Valentine', 'Ender'];
for (const i = 0; i < wiggins.length; i++) {
console.log(wiggins[i]);
}
After one iteration (that writes “Peter” to the output) this code raises a TypeError
with the “Assignment to constant variable” message. The reason for this behavior is that the i++
expression is about to change the value of i
.
Semantically, the engine represents the for-loop as two nested blocks. The outer block contains the loop variable initialization, the condition check, and the update of the loop variable. The inner block is the body of the for-loop that executes in each iteration.
Because the loop variable is in the outer block, you cannot update it, and thus the engine raises the TypeError
as the loop is about to carry out the second iteration.
There are two other loop constructs in JavaScript, the for-in-loop, and the for-of-loop, respectively. Interestingly, you can use const
declaration in them. This sample demonstrates the for-in-loop:
// Exercise-01-16.js
const petraStats = {
name: 'Petra',
wins: 12,
rank: 4
};
for(const key in petraStats) {
console.log(key, '=', petraStats[key]);
}
Here, the code does not reassign the key iteration variable. The scope of the for-in-loop’s iteration variable is the iteration block. Internally, the JavaScript engine initializes key to the corresponding string value in each iteration.
Note: ES 2015 has a new construct, the iterator. The for-of-loop works with iterators, as you will learn about it in a future article.
Mending For-loops with let
JavaScript closures often make a game on developers when using in a loop:
// Exercise-01-17.js
let names = ['Salamander', 'Rat', 'Rabbit', 'Dragon']
let armyMakers = [];
populateArmyMakers();
for (var i = 0; i < armyMakers.length; i++) {
console.log(armyMakers[i]());
}
function populateArmyMakers() {
for (var i = 0; i < names.length; i++) {
armyMakers.push(function() {
return names[i] + ' army';
})
}
}
The populateArmyMakers
function creates an array of five functions that seem to return “… army” strings. However, when you run this code, it displays “undefined army” five times. The reason for this behavior is that i
is a captured by the army-name-creator function, and is evaluated only when the generated function runs. Each function stored in armyMakers
uses the final value of i
after completing the loop, namely 4. Because the length of names
is 4, names[4]
retrieves an undefined value.
Seasoned developers know the remedy: instead of a simple function, an immediately invoked function expression (IIFE) should be used:
// Exercise-01-18.js
let sqrFunctions = [];
populateSqrFunctions();
for (var i = 0; i < sqrFunctions.length; i++) {
console.log(sqrFunctions[i]());
}
function populateSqrFunctions() {
for (var i = 0; i < 5; i++) {
sqrFunctions.push(
(function(n) {
return function() {
return n*n
};
})(i));
}
}
As this short code snippet shows, IIFEs are not easy to read. There are too many braces and parentheses that make it difficult to follow the code.
The let
declaration makes this hocus-pocus unnecessary. When you run this code, the output displays the expected army names:
// Exercise-01-19.js
let names = ['Salamander', 'Rat', 'Rabbit', 'Dragon']
let armyMakers = [];
populateArmyMakers();
for (var i = 0; i < armyMakers.length; i++) {
console.log(armyMakers[i]());
}
function populateArmyMakers() {
for (let i = 0; i < names.length; i++) {
armyMakers.push(function() {
return names[i] + ' army';
})
}
}
Because the scope of i
is the for-loop, the engine creates a clone of i
in each iteration, and the newly created function captures this cloned value. As i
changes, so does the value of the captured clone. Due to the let
declaration, the output of this code is:
Salamander army
Rat army
Rabbit army
Dragon army
Functions and the TDZ
Function hoisting and block-level variables sometimes may deceive you. For example, you may think the following code snippet displays “Ender”:
// Exercise-01-20.js
function setCommander() {
commander = 'Bean'
}
let commander = 'Ender';
setCommander();
console.log('The commander is', commander);
However, it displays “Bean”. After dealing so much with let
and block-level variable declarations, you unconsciously may think the commander variable used within the setCommander
function is a local—and a block-level—variable. So, setting it to “Bean” does not affect commander
declared with let right after the function.
Well, commander
used in the function is the same variable declared outside of it. Do not let it deceive you!
When the JavaScript engine parses this code, it first scans for variables and then hoists functions. By the time setCommander
is analyzed, the engine knows commander
. During code execution, when the flow reaches the let
declaration, commander
is removed from the TDZ. The invocation of setCommander
assigns “Bean” to the variable.
Hint: Try what output this code snippet produces when you change the lines of variable declaration and setCommander()
invocation.
How to Move from var to let and const
With ECMAScript 2015, the let
declaration works as var
should have always behaved. Thus, it seems to be a good idea to change all var
declarations to let
as you adopt ES2015. Nonetheless, due to the semantic differences between var
and let
may prevent your modified code to work after changes.
Provided, you have automated tests, you can mitigate this risk. However, if you feel that the test coverage is not sufficient, be careful with such changes. Change var
to let
in small steps and check that the affected code still works.
There is another—even bolder—approach. When you face with a var
declaration that has an initialization expression, change it to const
. If the code reassigns a value to it, you will get a TypeError
with the “Assignment to constant variable” message. Should you get this message, you would know that you cannot apply const
but let
.
Many developers of the ES 2015 camp have already tried this approach. The feedback from JavaScript communities points out that this practice is viable and useful. Most variables do not change their values after the initial assignment, thus using const
can be an excellent tool to prevent unexpected changes that would lead to bugs.
Summary
The new let
and const
declarations allow you to create variables with a block-level scope. In contrast to var
, these do not hoist variable declarations to the top of their scope (function or global), and thus eliminate a couple of annoying behaviors of var
.
With const
, you declare variables that are assigned to an initial value only. When you try to reassign them to a new value, the JavaScript engine raises a TypeError
. Be careful with object values: const
does not prevent you from changing a property of a const-assigned object.
With let
and const
, you can declare your variables in the narrowest scope—the innermost block—where you need them, and it may help you avoid unexpected features and bugs coming from the floppy behavior of var
.