let and const Commands
# let and const Commands
# let Command
# Basic Usage
ES6 introduced the let command for declaring variables. Its usage is similar to var, but the declared variable is only valid within the code block where the let command resides (block scope).
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
2
3
4
5
6
7
In the above code, two variables are declared inside a code block using let and var respectively. When these two variables are called outside the code block, the let-declared variable throws an error while the var-declared variable returns the correct value. This shows that let-declared variables are only valid within the code block where they are declared.
The for loop counter is a great use case for the let command.
for (let i = 0; i < 10; i++) {
// ...
}
console.log(i);
// ReferenceError: i is not defined
2
3
4
5
6
In the above code, the counter i is only valid inside the for loop body. Referencing it outside the loop body results in an error.
If
varis used to declareiinside the for loop, it would print 10
The following code, if using var, would output 10 at the end.
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () { // During the loop, the function is only assigned to a[i], not executed. The i inside the function is the global i
console.log(i);
};
}
a[6](); // 10
2
3
4
5
6
7
In the above code, the variable i is declared with var and is valid globally, so there is only one variable i in the entire global scope. In each iteration, the value of i changes, and the console.log(i) inside the functions assigned to array a -- the i inside refers to the global i. In other words, the i in all members of array a points to the same i, causing the output at runtime to be the value of i from the last iteration, which is 10.
If let is used, the declared variable is only valid within the block scope, and the final output is 6.
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
2
3
4
5
6
7
In the above code, the variable i is declared with let. The current i is only valid in the current iteration, so each iteration's i is actually a new variable, which is why the final output is 6. You might ask, if the variable i is redeclared in each iteration, how does it know the value from the previous iteration to compute the current iteration's value? This is because the JavaScript engine internally remembers the value from the previous iteration and initializes the current iteration's variable i based on the previous iteration.
Additionally, for loops have a special characteristic: the part that sets the loop variable is a parent scope, while the loop body is a separate child scope.
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
2
3
4
5
6
7
The above code runs correctly and outputs abc three times. This shows that the variable i inside the function and the loop variable i are not in the same scope -- they each have their own separate scope.
# No Variable Hoisting
The var command causes "variable hoisting," meaning a variable can be used before it is declared, with its value being undefined. This behavior is somewhat odd. Logically, a variable should only be usable after its declaration statement.
To correct this behavior, the let command changes the syntax behavior so that the variables it declares must be used after declaration; otherwise, an error is thrown.
// var case
console.log(foo); // outputs undefined
var foo = 2;
// let case
console.log(bar); // ReferenceError
let bar = 2;
2
3
4
5
6
7
In the above code, the variable foo is declared with var, so variable hoisting occurs -- when the script starts running, the variable foo already exists but has no value, hence outputting undefined. The variable bar is declared with let, so no variable hoisting occurs. This means that before it is declared, the variable bar does not exist, and using it at that point will throw an error.
# Temporal Dead Zone
As long as a let command exists within a block scope, the variable it declares is "bound" to that region and is no longer affected by the outside.
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
2
3
4
5
6
In the above code, a global variable tmp exists, but let also declares a local variable tmp inside the block scope. This causes the latter to be bound to this block scope, so assigning a value to tmp before the let declaration throws an error.
ES6 explicitly states: If let and const commands exist within a block, the variables declared by these commands form a closed scope from the very beginning. Using these variables before their declaration will result in an error.
In summary, within a code block, using the let command to declare a variable makes that variable unavailable before the declaration. This is syntactically called the "temporal dead zone" (TDZ).
if (true) {
// TDZ starts
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ ends
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
2
3
4
5
6
7
8
9
10
11
In the above code, before the let command declares the variable tmp, everything belongs to the variable tmp's "dead zone."
The "temporal dead zone" also means that typeof is no longer a 100% safe operation.
typeof x; // ReferenceError
let x;
2
In the above code, the variable x is declared with let, so before the declaration, everything belongs to x's "dead zone," and using the variable will throw an error. Therefore, typeof throws a ReferenceError at runtime.
By comparison, if a variable has never been declared at all, using typeof does not throw an error.
typeof undeclared_variable // "undefined"
In the above code, undeclared_variable is a non-existent variable name, yet the result is "undefined." So before let existed, the typeof operator was 100% safe and would never throw an error. This is no longer the case. This design encourages good programming habits -- variables should always be used after declaration; otherwise, an error is thrown.
Some "dead zones" are relatively hidden and not easy to spot.
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // throws error
2
3
4
5
In the above code, the reason calling bar throws an error (some implementations may not) is that parameter x's default value equals another parameter y, but y has not been declared yet, making it a "dead zone." If y's default value were x, it wouldn't throw an error because x has already been declared.
function bar(x = 2, y = x) {
return [x, y];
}
bar(); // [2, 2]
2
3
4
Additionally, the following code also throws an error, behaving differently from var.
// No error
var x = x;
// Error
let x = x;
// ReferenceError: x is not defined
2
3
4
5
6
The above code throws an error also because of the temporal dead zone. When declaring a variable with let, if the variable is used before the declaration statement is complete, an error is thrown. The line above falls into this situation -- it tries to get the value of x before the declaration statement for variable x has finished executing, resulting in the "x is not defined" error.
ES6 specifies that temporal dead zones and the fact that let and const statements do not cause variable hoisting are mainly to reduce runtime errors and prevent the use of a variable before it is declared, which can lead to unexpected behavior. Such errors were very common in ES5, and now with this rule, avoiding such errors is much easier.
In summary, the essence of the temporal dead zone is that as soon as you enter the current scope, the variable you intend to use already exists, but it cannot be accessed. Only when the line of code that declares the variable appears can you access and use that variable.
# No Duplicate Declarations
let does not allow duplicate declarations of the same variable within the same scope.
// Error
function func() {
let a = 10;
var a = 1;
}
// Error
function func() {
let a = 10;
let a = 1;
}
2
3
4
5
6
7
8
9
10
11
Therefore, you cannot redeclare parameters inside a function.
function func(arg) {
let arg;
}
func() // Error
function func(arg) {
{
let arg;
}
}
func() // No error
2
3
4
5
6
7
8
9
10
11
# Block Scope
# Why Is Block Scope Needed?
ES5 only has global scope and function scope, without block scope, which leads to many unreasonable scenarios.
The first scenario is that inner variables may override outer variables.
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
2
3
4
5
6
7
8
9
10
The original intention of the above code is to use the outer tmp variable outside the if block and the inner tmp variable inside it. However, after function f executes, the output is undefined because variable hoisting causes the inner tmp variable to override the outer tmp variable.
The second scenario is that loop counter variables leak into the global scope.
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
2
3
4
5
6
7
In the above code, the variable i is only used to control the loop, but after the loop ends, it doesn't disappear -- it leaks into the global scope.
# ES6 Block Scope
let effectively adds block scope to JavaScript.
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
2
3
4
5
6
7
The above function has two code blocks, both declaring a variable n. Running it outputs 5, showing that the outer code block is not affected by the inner code block. If var were used to define n in both cases, the final output would be 10.
ES6 allows arbitrary nesting of block scopes.
{{{{
{let insane = 'Hello World'}
console.log(insane); // Error
}}}};
2
3
4
The above code uses five levels of block scope, each being a separate scope. The fourth level scope cannot read the internal variables of the fifth level scope.
Inner scopes can define variables with the same name as outer scopes.
{{{{
let insane = 'Hello World';
{let insane = 'Hello World'}
}}}};
2
3
4
The advent of block scope effectively makes the widely used anonymous Immediately Invoked Function Expression (IIFE) unnecessary.
// IIFE approach
(function () {
var tmp = ...;
...
}());
// Block scope approach
{
let tmp = ...;
...
}
2
3
4
5
6
7
8
9
10
11
# Block Scope and Function Declarations
Can functions be declared inside block scopes? This is quite a confusing question.
ES5 states that functions can only be declared in top-level scope and function scope, not in block scope.
// Case 1
if (true) {
function f() {}
}
// Case 2
try {
function f() {}
} catch(e) {
// ...
}
2
3
4
5
6
7
8
9
10
11
Both function declarations above are illegal according to ES5.
However, browsers did not comply with this rule. To maintain compatibility with older code, they still supported function declarations in block scopes, so both cases above actually work and do not throw errors.
ES6 introduced block scope and explicitly allows function declarations inside block scopes. ES6 states that within block scopes, function declaration statements behave like let and cannot be referenced outside the block scope.
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// Redeclare function f
function f() { console.log('I am inside!'); }
}
f();
}());
2
3
4
5
6
7
8
9
10
Running the above code in ES5 would produce "I am inside!" because the function f declared inside if gets hoisted to the top of the function. The actual code that runs is as follows.
// ES5 environment
function f() { console.log('I am outside!'); }
(function () {
function f() { console.log('I am inside!'); }
if (false) {
}
f();
}());
2
3
4
5
6
7
8
9
ES6 is completely different -- theoretically it should produce "I am outside!" because function declarations inside block scopes behave like let and don't affect anything outside the scope. However, if you actually run the above code in an ES6 browser, it will throw an error. Why?
// Browser ES6 environment
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// Redeclare function f
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
2
3
4
5
6
7
8
9
10
11
12
The above code throws an error in ES6 browsers.
The reason is that changing the rules for handling function declarations inside block scopes would obviously have a major impact on older code. To mitigate compatibility issues, ES6 specifies in Annex B (opens new window) that browser implementations may deviate from the above rules and have their own behavior (opens new window).
- Function declarations are allowed inside block scopes.
- Function declarations behave like
var, meaning they are hoisted to the top of the global scope or function scope. - Additionally, function declarations are also hoisted to the top of the block scope they are in.
Note that the above three rules only apply to browser implementations of ES6. Other environment implementations do not need to follow these rules and should still treat block-scoped function declarations as let.
Based on these three rules, in browser ES6 environments, functions declared inside block scopes behave like var-declared variables. The above example actually runs the following code.
// Browser ES6 environment
function f() { console.log('I am outside!'); }
(function () {
var f = undefined;
if (false) {
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
2
3
4
5
6
7
8
9
10
11
Given the significant behavioral differences across environments, you should avoid declaring functions inside block scopes. If you must, use function expressions rather than function declaration statements.
// Function declaration statement inside block scope (not recommended)
{
let a = 'secret';
function f() {
return a;
}
}
// Function expression inside block scope (preferred)
{
let a = 'secret';
let f = function () {
return a;
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Additionally, another point to note: ES6 block scopes must have curly braces. Without curly braces, the JavaScript engine considers that no block scope exists.
// First approach, throws error
if (true) let x = 1;
// Second approach, no error
if (true) {
let x = 1;
}
2
3
4
5
6
7
In the above code, the first approach has no curly braces, so no block scope exists. Since let can only appear at the top level of the current scope, it throws an error. The second approach has curly braces, so block scope is established.
The same applies to function declarations. In strict mode, functions can only be declared at the top level of the current scope.
// No error
'use strict';
if (true) {
function f() {}
}
// Error
'use strict';
if (true)
function f() {}
2
3
4
5
6
7
8
9
10
# const Command
# Basic Usage
const declares a read-only constant. Once declared, the value of the constant cannot be changed.
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
2
3
4
5
The above code shows that changing the value of a constant throws an error.
Since const-declared variables cannot change their values, this means that const variables must be initialized immediately upon declaration -- they cannot be left for later assignment.
const foo;
// SyntaxError: Missing initializer in const declaration
2
The above code shows that for const, declaring without assigning throws an error.
The scope of const is the same as let: it is only valid within the block scope where it is declared.
if (true) {
const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not defined
2
3
4
5
Constants declared with const are also not hoisted and similarly have a temporal dead zone -- they can only be used after the position where they are declared.
if (true) {
console.log(MAX); // ReferenceError
const MAX = 5;
}
2
3
4
The above code calls the constant MAX before its declaration and throws an error.
Constants declared with const, like let, cannot be declared again.
var message = "Hello!";
let age = 25;
// Both lines below throw errors
const message = "Goodbye!";
const age = 30;
2
3
4
5
6
# The Essence
What const actually guarantees is not that the variable's value cannot change, but that the data stored at the memory address the variable points to cannot change. For simple data types (numbers, strings, booleans), the value is stored directly at the memory address the variable points to, making it equivalent to a constant. However, for compound data types (mainly objects and arrays), the memory address the variable points to only stores a pointer to the actual data. const can only guarantee that this pointer is fixed (i.e., it always points to another fixed address), but it cannot control whether the data structure it points to is mutable. Therefore, you must be very careful when declaring an object as a constant.
const foo = {};
// Adding a property to foo succeeds
foo.prop = 123;
foo.prop // 123
// Pointing foo to another object throws an error
foo = {}; // TypeError: "foo" is read-only
2
3
4
5
6
7
8
In the above code, the constant foo stores an address that points to an object. What is immutable is only this address -- you can't point foo to another address -- but the object itself is mutable, so you can still add new properties to it.
Here is another example.
const a = [];
a.push('Hello'); // works
a.length = 0; // works
a = ['Dave']; // Error
2
3
4
In the above code, the constant a is an array. The array itself is writable, but assigning another array to a throws an error.
If you truly want to freeze an object, use the Object.freeze method.
const foo = Object.freeze({});
// In non-strict mode, the following line has no effect;
// In strict mode, it throws an error
foo.prop = 123;
2
3
4
5
In the above code, the constant foo points to a frozen object, so adding new properties has no effect, and in strict mode, it throws an error.
Besides freezing the object itself, the object's properties should also be frozen. Here is a function that completely freezes an object.
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
2
3
4
5
6
7
8
# Six Ways to Declare Variables in ES6
ES5 has only two ways to declare variables: the var command and the function command. In addition to let and const, ES6 also introduces two more ways to declare variables (covered in later chapters): the import command and the class command. So ES6 has a total of 6 ways to declare variables.
# Properties of the Top-Level Object
The top-level object refers to the window object in browser environments and the global object in Node. In ES5, properties of the top-level object are equivalent to global variables.
window.a = 1;
a // 1
a = 2;
window.a // 2
2
3
4
5
In the above code, assigning to a property of the top-level object is the same as assigning to a global variable.
The coupling between top-level object properties and global variables is considered one of the biggest design flaws of the JavaScript language. This design creates several major problems: first, it's impossible to report undeclared variable errors at compile time, as errors can only be detected at runtime (because global variables may be created as properties of the top-level object, and property creation is dynamic); second, programmers can easily create global variables unintentionally (e.g., through typos); and third, top-level object properties are readable and writable everywhere, which is very unfavorable for modular programming. On the other hand, the window object has a concrete meaning, referring to the browser window object, and having the top-level object be an object with concrete meaning is also inappropriate.
To address this, ES6 specifies that, for backward compatibility, global variables declared with var and function commands remain properties of the top-level object. However, global variables declared with let, const, and class commands are not properties of the top-level object. In other words, starting from ES6, global variables will gradually be decoupled from top-level object properties.
var a = 1;
// In Node's REPL environment, this can be written as global.a
// Or using the universal method, written as this.a
window.a // 1
let b = 1;
window.b // undefined
2
3
4
5
6
7
In the above code, the global variable a is declared with var, so it is a property of the top-level object; the global variable b is declared with let, so it is not a property of the top-level object and returns undefined.
# The globalThis Object
The JavaScript language has a top-level object that provides the global environment (i.e., global scope) in which all code runs. However, the top-level object is not consistent across different implementations.
- In browsers, the top-level object is
window, but Node and Web Workers do not havewindow. - In browsers and Web Workers,
selfalso refers to the top-level object, but Node does not haveself. - In Node, the top-level object is
global, but other environments do not support it.
To get the top-level object in all environments with the same code, the this variable is commonly used, but it has limitations.
- In the global environment,
thisreturns the top-level object. However, in Node modules and ES6 modules,thisreturns the current module. - Inside a function, if the function is not run as a method of an object but simply as a function,
thispoints to the top-level object. However, in strict mode,thisreturnsundefined. - Regardless of strict or non-strict mode,
new Function('return this')()always returns the global object. However, if the browser uses CSP (Content Security Policy), theneval,new Function, and similar methods may not be usable.
In summary, it is very difficult to find a method that can get the top-level object in all cases. Here are two methods that can barely be used.
// Method 1
(typeof window !== 'undefined'
? window
: (typeof process === 'object' &&
typeof require === 'function' &&
typeof global === 'object')
? global
: this);
// Method 2
var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ES2020 (opens new window) introduces globalThis at the language standard level as the top-level object. This means that in any environment, globalThis exists and can be used to get the top-level object, pointing to this in the global environment.
The polyfill library global-this (opens new window) simulates this proposal, allowing globalThis to be used in all environments.