post-css-position

Powerful JavaScript Variable Declaration Tutorial: ES6 Standard Let and Var (2019)

To use a variable, you must first declare it. This article introduces JavaScript variable declaration. We talk about the differences between let and var, the variable scope, and much more. Many useful examples are given.

The Let Command

Basic Usage

ES6 has added the let command to declare variables. Its usage is similar to var, but the declared variable is only valid within the code block in which the let command is located.

{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1

In the above code, we declare two variables with let and var, respectively. These two variables are then used outside the code block, and the variable declared by let is reported incorrectly, while the var declared variable returns the correct value. This means that the let declared variable is only valid in its code block.

The counter of the loop for is very suitable for using the let command.

for (let i = 0; i < 10; i++) {
  // ...
}

console.log(i);
// ReferenceError: i is not defined

In the above code, the counter i is only valid in the for loop body. An error will be reported when the counter is referenced outside the loop.

If the following code, we use the var statement, which obtains the final output 10.

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

In the above code, the variable i is declared by the var command and is valid in the global scope. While i increased from 0 to 9, a[i] points to a function that outputs the value of the global variable i.

If we use let, the declared variable is only valid at the block level 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

In the above code, the variable i is let declared. In each round of the loop, the current i is only valid in this round. So each i is actually a new variable. Thus the final output is 6.

You may ask, if the variable i of each round is re-declared, how does it know the value of the previous round to calculate the value of this round? This is because the JavaScript engine internally remembers the value of the previous round of loop. When the variable i of this round is initialized , it is calculated based on the previous round of loop.

In addition, the for loop has a special feature, that is, the part that sets the loop variable is a parent scope, and the inside of the loop body is a separate child scope.

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

The above code runs correctly and outputs abc 3 times. This means that the counter variable i and the internal variable i are not in the same scope.

No Variable Promotion

The var command has a “variable promotion” phenomenon, that is, the variable can be used before the declaration, and the value is undefined. This phenomenon is somewhat strange.

Typically, variables should be used after the statement is declared.

In order to correct this phenomenon, the let command changes the grammatical behavior. All variables declared by the let command must be used after the declaration, otherwise an error will be reported.

// var
console.log(foo); // undefined
var foo = 2;

// let
console.log(bar); // ReferenceError
let bar = 2;

In the above code, the variable foo is declared with the var command, and the variable promotion occurs. That is, when the script starts running, the variable foo already exists, but there is no value, so it will output undefined. The variables bar is declared with the let command and no variable promotion occurs. This means that the variable bar does not exist until it is declared, and if it is used, an error will be thrown.

Temporary Dead Zone

As long as there is a let command in the block-level scope, the variable it declared will “bind” this area and is no longer affected by the outside.

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

In the above code, there is a global variable tmp, but a local variable tmp is declared by let in the block-level scope, causing the local one to bind to the block-level scope. So if you use the local variable tmp before declaring it, an error will be throwed.

IMPORTANT: ES6 clearly states that if there are let and const commands in a block, the variables declared by these commands in the block form a closed scope from the beginning. Anyone who uses these variables before the statement will report an error.

In summary, within a code block, a variable is not available until the variable is declared using the let command. This is syntactically called a “temporal dead zone” (TDZ).

if (true) {
  // TDZ begins
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ ends
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

In the above code, before declaring a variable tmp with the let command, all previous lines of codes in the block belong to the “dead zone” of the variable tmp.

“Temporary dead zone” also means typeof is no longer a 100% safe operation.

typeof x; // ReferenceError
let x;

In the above code, the variable x uses the let command declaration. So before the declaration, all codes in the same level belong to the “dead zone” of x. As long as the variable is used, it will report an error. Therefore, typeof will throw a runtime error ReferenceError.

As a comparison, if a variable is not declared at all, typeof will not give an error.

typeof undeclared_variable // "undefined"

In the above code, undeclared_variable is a variable name that does not exist, and the result returns “undefined”. So, before the let keyword is added, the typeof operator was 100% safe and never report an error. However, this is not true now. This design is to let everyone develop good programming habits: variables must be used after the statement, otherwise it will be reported.

Some “dead zones” are more subtle and less likely to be discovered.

function bar(x = y, y = 2) {
  return [x, y];
}

bar(); // Error

In the above code, the function bar reports an error (some implementations may not report an error), because the default value of the parameter x is equal to another parameter y, where y has not been declared at this time. If the default value of  y is x, no error will be reported because x has already been declared.

function bar(x = 2, y = x) {
  return [x, y];
}
bar(); // [2, 2]

In addition, the following code will also report an error, which is different from the var behavior.

// Correct
var x = x;

// Error
let x = x;
// ReferenceError: x is not defined

The above code is reported incorrectly because of the temporary dead zone. When using a let declarative variable, an error is reported as long as the variable is used before it has been declared. The above line belongs to this case. Before the declaration statement of the variable x, the value x is fetched first, resulting in an error “x undefined”.

ES6 stipulates that temporary dead zones and let and const statements do prohibit variable promotion. They mainly try to reduce runtime errors and prevent the use of variables before the variable is declared, resulting in unexpected behavior. Such errors are common in ES5, and with this now, it’s easy to avoid such errors.

In short, the essence of a temporary dead zone is that as soon as you enter the current scope, the variable you want to use already exists, but it is not available. You can only get and use the variable when the line that declares the variable is executed.

Duplicate Declarations are Not Allowed

It is not allowed to declare the same variable repeatedly within the same scope with the let command.

// Error
function func() {
  let a = 10;
  var a = 1;
}

// Error
function func() {
  let a = 10;
  let a = 1;
}

Therefore, you cannot redeclare parameters inside a function.

function func1(arg) {
  let arg;
}
func1() // Error

function func2(arg) {
  {
    let arg;
  }
}
func2() // Correct

Block Level Scope

Why do you need a block-level scope?

ES5 has only global scope and function scope, and no block-level scope, which brings a lot of unreasonable scenarios.

In the first scenario, inner variables may override outer variables.

var tmp = new Date();

function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}

f(); // undefined

The original meaning of the above code is that the outer part of the if code block uses the outer variable tmp and the inner part use the inner variable tmp, respectively. However, the output of the function f is undefined. Because the variable is promoted, causing the inner tmp variable to cover the outer tmp variable.

In the second scenario, the loop variable used for counting is leaked as a global variable.

var s = 'hello';

for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}

console.log(i); // 5

In the above code, the variable i is only used to control the loop, but after the loop is over, it does not disappear. And the counter i becomes a global variable.

Block-level scope of ES6

The let keyword actually creates a new block-level scope for JavaScript.

function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}

The above function has two code blocks, all of which declare variables n, and output 5 after running. This means that the outer code block is not affected by the inner code block. If you use var to define the variable n twice, the output value is 10.

ES6 allows arbitrary nesting of block-level scopes.

{{{{
  {let insane = 'Hello World'}
  console.log(insane); // Error
}}}};

The above code uses a five-level block-level scope, each layer being a separate scope. The fourth level scope cannot read the internal variables of the fifth level scope.

The inner scope can define the same-named variable of the outer scope.

{{{{
  let insane = 'Hello World';
  {let insane = 'Hello World'}
}}}};

The emergence of block-level scopes actually makes it unnecessary to adopt widely used anonymous immediate execution function expressions (anonymous IIFE).

// IIFE
(function () {
  var tmp = ...;
  ...
}());

// block-level scop
{
  let tmp = ...;
  ...
}

Block-level scope and function declaration

Can a function be declared in a block-level scope? This is a rather confusing issue.

ES5 states that functions can only be declared in the top-level scope and function scope, and cannot be declared at the block-level scope.

// Situation one
if (true) {
  function f() {}
}

// Situation two
try {
  function f() {}
} catch(e) {
  // ...
}

The above two function declarations are illegal according to the rules of ES5.

However, the browser does not comply with this rule. In order to be compatible with the old code, it is still supported to declare the function in the block-level scope. Therefore, the above two cases can actually run without error.

ES6 introduces a block-level scope that explicitly allows functions to be declared in a block-level scope. ES6 stipulates that among block-level scopes, function declaration statements behave similarly to let and cannot be referenced outside of block-level scope.

function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // Repeat the statement function f
    function f() { console.log('I am inside!'); }
  }

  f();
}());

If we run the above code in ES5, we get “I am inside!”. Because the declaration of the function f will be raised to the head of the anonymous function. Actually, the following code is running.

// ES5 environment
function f() { console.log('I am outside!'); }

{
  function f() { console.log('I am inside!'); }
  if (false) {
  }
  f();
}

The ES6 is completely different, and in theory it will get “I am outside!” Because the function is declared in the block-level scope. However, if you actually run the above code in the ES6 browser, you will get an error. Why?

// browser's ES6 environment
function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // Repeat the statement function f
    function f() { console.log('I am inside!'); }
  }

  f();
}());
// Uncaught TypeError: f is not a function

The above code will report an error in the ES6 browser.

It turns out that if you change the processing rules of functions declared in the block-level scope, it will obviously have a big impact on the old code. In order to alleviate the resulting incompatibility issues, ES6 specifies in Appendix B that browser implementations may not follow the above rules and have their own behavior.

  • Allows declaration of functions within a block-level scope.
  • Function declarations are similar to var, that is, they are promoted to the head of the global scope or function scope.
  • At the same time, the function declaration is promoted to the head of the block-level scope in which it resides.

Note that the above three rules are only valid for ES6 browser implementations. Other environments need not to obey, meaning that block-scoped function declarations are still treated as let.

According to these three rules, in a browser’s ES6 environment, functions declared within a block-level scope behave like var-declared variables. The code for the above example actually runs as follows.

// browser's 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

Considering that the behavioral differences caused by the environment are too large, you should avoid declaring functions in the block-level scope. If you really need it, you should write a function expression instead of a function declaration statement.

// Function declaration statements inside the block-level scope, it is recommended not to use
{
  let a = 'secret';
  function f() {
    return a;
  }
}

// Inside the block-level scope, use function expressions first
{
  let a = 'secret';
  let f = function () {
    return a;
  };
}

In addition, there is an issue to pay attention to. The block-level scope of ES6 must have curly braces. If there are no braces, the JavaScript engine assumes that there is no block-level scope.

// The first way of writing, giving an error
if (true) let x = 1;

// The second way of writing, no error
if (true) {
  let x = 1;
}

In the above code, the first type of writing has no braces, so there is no block-level scope. The second way of writing has braces, so the block-level scope is created.

The same is true for 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() {}

The Const Command

Basic usage

The const keyword 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.

The above code indicates that changing the value of a constant will report an error.

Once a const variable is declared, it must be initialized immediately and cannot be re-assigned to a another value later.

const foo;
// SyntaxError: Missing initializer in const declaration

The above code indicates that, a const declaration without assignment, will throw an error.

The scope of const is the same as the let command: it is only valid within the block-level scope in which the declaration is located.

if (true) {
  const MAX = 5;
}

MAX // Uncaught ReferenceError: MAX is not defined

The constants declared by the const command are also not promoted. There is also a temporary dead zone, which can only be used after the declared location.

if (true) {
  console.log(MAX); // ReferenceError
  const MAX = 5;
}

In the above code, MAX is used before the constant declaration. An error is thrown.

The const-declared constants are also non-repeatable.

var message = "Hello!";
let age = 25;

// The following two lines will report an error
const message = "Goodbye!";
const age = 30;

The essence of const

In fact, const guarantee that the data stored in the memory address pointed by the variable must not be changed. For simple types of data (numeric, string, boolean), the value is stored in the memory address pointed by the variable, and is therefore equivalent to a constant.

But for composite type data (mainly objects and arrays), the memory address pointed by the variable saves only a pointer to the actual data. And const only guarantees that the pointer is fixed (i.e., always points to another fixed address). As for the content of the data structure, it is completely out of control. Therefore, you must be very careful when declaring an object as a constant.

const foo = {};

// Add an attribute to foo that will succeed
foo.prop = 123;
foo.prop // 123

// Point foo to another object and get an error
foo = {}; // TypeError: "foo" is read-only

In the above code, the constant foo stores an address that points to an object. This address is immutable, that is, you can’t change foo to point to another address. But the object itself is mutable, so you can still add new properties to it.

Here’s another example.

const a = [];
a.push('Hello'); // executable
a.length = 0; // executable
a = ['Dave']; // error

In the above code, the constant a is an array. The array itself is writable. But if another array is assigned to a, it will report an error.

If you really want to freeze the object, you should use the Object.freeze method.

const foo = Object.freeze({});

// In normal mode, the following line does not work;
// In strict mode, the line will report an error
foo.prop = 123;

In the above code, the constant foo points to a frozen object, so adding a new property does not work, and an error is reported in strict mode.

In addition to freeze the object itself, the properties of the object should also be frozen. Below 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] );
    }
  });
};

Six ways to declare variables in ES6

ES5 has only two methods for declaring variables: var and function commands. ES6 adds let and const commands, there are two other ways to declare variables: import and class commands. So, ES6 has a total of six methods for declaring variables:

  1. var
  2. function
  3. let
  4. const
  5. import
  6. class

Properties of Top-Level Object

The top-level object, in the browser environment, refers to the window object, and in Node refers to the global object. Among ES5, the properties of the top-level object are equivalent to global variables.

window.a = 1;
a // 1

a = 2;
window.a // 2

In the above code, the attribute assignment of the top-level object is the same as the assignment of the global variable.

The properties of the top-level object are tied to global variables and are considered to be one of the biggest design failures of the JavaScript language. This design brings a lot of big problems.

  • First, it is impossible to report the undeclared errors of the variables at compile time. Only the runtime can know (because the global variables may be created by the properties of the top-level object, and the properties’ creation is dynamic);
  • Second, it is easy for programmers to unwittingly create global variables (such as typing errors);
  • Third, the properties of top-level objects are readable and writable everywhere, which is not conducive to modular programming.
  • Finally, an window object has an entity meaning, which refers to the window object of the browser. The top-level object is an object with an entity meaning, which is also inappropriate.

ES6 stipulates that, in order to maintain compatibility, global variables declared by var and function commands are still attributes of top-level objects; on the other hand, global variables declared by letconst and class commands are not part of the top-level object. That is, starting with ES6, global variables are gradually decoupled from the properties of the top-level object.

var a = 1;
// If in the REPL environment of Node, you can write global.a
// Or use a generic method, write this.a
window.a // 1

let b = 1;
window.b // undefined

In the above code, the global variable a is declared by the var command, so it is the property of the top-level object. The global variable b is declared by the let command, so it is not the property of the top-level object.

globalThis object

The JavaScript language has a top-level object that provides a global environment (i.e., the global scope) in which all code runs. However, top-level objects are not uniform across implementations.

  • Inside the browser, the top-level object is window, but Node and Web Worker do not have window.
  • Inside the browser and Web Worker, self also points to the top-level object, but Node does not have self.
  • In the Node, the top-level object is global, but other environments do not support it.

In order to be able to get top-level objects in various environments, a popular solution is using the this variable, but there are limitations.

  • In the global environment, the this top-level object is returned. However, in the Node module and the ES6 module, the current module is returned.
  • Inside a function, if the function is not run as an object method, but simply as a function, this will point to the top-level object. However, in strict mode, this will return undefined.
  • Whether it is strict mode or normal mode, new Function('return this')() always returns a global object. However, if the browser has the CSP (Content Security Policy), then the evalnew Function methods may not be available.

In summary, it is difficult to find a way to get the top-level object in all cases. Here are two ways you can barely use it.

// method one
(typeof window !== 'undefined'
   ? window
   : (typeof process === 'object' &&
      typeof require === 'function' &&
      typeof global === 'object')
     ? global
     : this);

// method two
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');
};

There is now a proposal that introduces globalThis as a top-level object at the level of language standards. In other words, in any environment, globalThis exists. You can get the top-level object from it, pointing to the global environment this.

The shim library global-this simulates this proposal and is available in all environments.

Closing Words

Amazon! This website has now 8 posts, each with a different topic. Have a look at some of the previous wonderful tutorials:

If you encounter any issue, please leave us a comment.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *