let and const commands

let command

Basic usage

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

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

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

The above code is in the code block, and two variables are declared with let and var respectively. Then call these two variables outside the code block, and the result is that the variable declared by let reports an error, and the variable declared by var returns the correct value. This shows that the variable declared by let is only valid in the code block in which it is located.

The counter of the for loop is very suitable to use 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 inside the for loop, and an error will be reported if it is referenced outside the loop.

If the following code uses var, the final output is 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, so there is only one variable i globally. Every time the loop, the value of the variable i will change, and the console.log(i) inside the function assigned to the array a in the loop, the i inside points to the global i . That is to say, all the i in the members of the array a all point to the same i, resulting in the output of the last round of i value at runtime, which is 10.

If you use let, the declared variable is only valid in 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 declared by let, the current i is only valid in the current round of the loop, so the i in each loop is actually a new variable, so the final output is 6 . You may ask, if the variable i in each loop is re-declared, how does it know the value of the previous loop and calculate the value of the current loop? This is because the JavaScript engine internally remembers the value of the previous cycle. When the variable i of this round is initialized, the calculation is performed on the basis of the previous cycle.

In addition, there is another special feature of the for loop, that is, the part where the loop variable is set 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 abc is output 3 times. This shows that the variable i inside the function and the loop variable i are not in the same scope, and have their own separate scopes.

Variable promotion does not exist

The var command will have a "variable promotion" phenomenon, that is, the variable can be used before the declaration, and the value is undefined. This phenomenon is more or less strange. According to the general logic, variables should be used after the statement statement.

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

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

// the case of let
console.log(bar); // Error ReferenceError
let bar = 2;

In the above code, the variable foo is declared with the var command, and variable promotion will occur. That is, when the script starts to run, the variable foo already exists but has no value, so undefined will be output. The variable bar is declared with the let command, and no variable promotion occurs. This means that the variable bar does not exist before it is declared, and if it is used at this time, an error will be thrown.

Temporary Dead Zone

As long as there is a let command in the block-level scope, the variables it declares are "bound" to this area and are no longer affected by external influences.

var tmp = 123;

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

In the above code, there is a global variable tmp, but in the block-level scope, let declares a local variable tmp, which causes the latter to bind to this block-level scope, so before the variable is declared by let, The assignment of tmp will report an error.

ES6 clearly stipulates that if there are let and const commands in a block, the variables declared by these commands in this block will form a closed scope from the beginning. If these variables are used before the declaration, an error will be reported.

In short, in the code block, the variable is not available until the variable is declared with the let command. This is grammatically called "temporal dead zone" (TDZ for short).

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
}

In the above code, before the let command declares the variable tmp, all belong to the "dead zone" of the variable tmp.

"Temporary dead zone" also means that typeof is no longer a 100% safe operation.

typeof x; // ReferenceError
let x;

In the above code, the variable x is declared using the let command, so before the declaration, it belongs to the "dead zone" of x, and an error will be reported as long as the variable is used. Therefore, a ReferenceError will be thrown when typeof runs.

As a comparison, if a variable is not declared at all, using typeof will not report 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". Therefore, before let, the typeof operator is 100% safe and will never report an error. This is not true now. This design is for everyone to develop good programming habits. Variables must be used after declaration, otherwise an error will be reported.

Some "dead zones" are relatively hidden and not easy to find.

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

bar(); // error

In the above code, the reason why the call to the bar function reports an error (some implementations may not report an error) is because the default value of the parameter x is equal to another parameter y, and at this time, the y has not been declared yet, which is a "dead" Area". 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 behavior of var.

// No error
var x = x;

// report an error
let x = x;
// ReferenceError: x is not defined

The above code reported an error, also because of the temporary dead zone. When using let to declare a variable, as long as the variable is used before the declaration is completed, an error will be reported. The above line belongs to this situation. Before the declaration statement of the variable x is executed, the value of x is taken, resulting in the error "x is not defined".

ES6 stipulates that the temporary dead zone and let and const statements do not have variable promotion, mainly to reduce runtime errors and prevent the use of this variable before the variable declaration, which may lead to unexpected behavior. Such mistakes are very common in ES5, and now with this kind of regulation, it is easy to avoid such mistakes.

In short, the essence of the temporary dead zone is that as soon as it enters the current scope, the variable to be used already exists, but it is not available. Only when the line of code that declares the variable appears, can the variable be obtained and used.

Duplicate declarations are not allowed

let is not allowed to declare the same variable repeatedly in the same scope.

// report an error
function func() {
  let a = 10;
  var a = 1;
}

// report an error
function func() {
  let a = 10;
  let a = 1;
}

Therefore, the parameters cannot be redeclared inside the function.

function func(arg) {
  let arg;
}
func(); // report an error

function func(arg) {
  {
    let arg;
  }
}
func(); // No error

Block scope

Why do we need block-level scope?

ES5 has only global scope and function scope, not block-level scope, which brings many 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 intention of the above code is that the outer tmp variable is used outside the if code block, and the inner tmp variable is used inside. However, after the function f is executed, the output result is undefined. The reason is the variable promotion, which causes 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 ends, it does not disappear and leaks into a global variable.

Block-level scope of ES6

let actually adds block-level scope to JavaScript.

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

The above function has two code blocks, both of which declare the variable 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 final 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 of which is a separate scope. The fourth-level scope cannot read the internal variables of the fifth-level scope.

The inner scope can define variables with the same name in the outer scope.

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

The emergence of block-level scope actually makes the widely used anonymous immediate execution function expression (anonymous IIFE) no longer necessary.

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

// Block-level scope writing
{
  let tmp = ...;
  ...
}

Block-level scope and function declaration

Can functions be declared in block-level scope? This is a rather confusing question.

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

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

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

The above two function declarations are illegal according to ES5 regulations.

However, the browser does not comply with this rule. In order to be compatible with the old code, it still supports the declaration of functions in the block-level scope, so the above two situations can actually run without error.

ES6 introduced block-level scope, which explicitly allows functions to be declared in block-level scope. ES6 stipulates that in the block-level scope, the function declaration statement behaves like let and cannot be referenced outside the block-level scope.

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

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

  f();
})();

If the above code runs in ES5, you will get "I am inside!", because the function f declared in the if will be promoted to the head of the function. The actual running code is as follows.

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

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

ES6 is completely different. In theory, you will get "I am outside!". Because the function declared in the block-level scope is similar to let, it has no effect outside the scope. However, if you really run the above code in an ES6 browser, you will get an error. Why?

// The ES6 environment of the browser
function f() {
  console.log("I am outside!");
}

(function () {
  if (false) {
    // Repeat 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 the processing rules of the functions declared in the block-level scope are changed, it will obviously have a great impact on the old code. In order to alleviate the resulting incompatibility issues, ES6 is listed in Appendix B stipulates that browser implementations can disobey the above regulations and have their own behavior.

-Allow functions to be declared in block-level scope. -Function declaration is similar to var, that is, it will be promoted to the head of global scope or function scope. -At the same time, the function declaration will also be promoted to the head of the block-level scope where it is located.

Note that the above three rules are only valid for ES6 browser implementations, implementations in other environments do not need to comply, and block-level scoped function declarations are still treated as let.

According to these three rules, in the ES6 environment of the browser, functions declared in the block-level scope behave similarly to variables declared by var. The actual running code of the above example is as follows.

// The ES6 environment of the browser
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 the large differences in behavior caused by the environment, you should avoid declaring functions in the block-level scope. If you really need it, it should be written as a function expression instead of a function declaration statement.

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

// Within the block-level scope, function expressions are preferred
{
  let a = "secret";
  let f = function () {
    return a;
  };
}

In addition, there is one more thing to pay attention to. The block-level scope of ES6 must have curly braces. If there are no curly braces, the JavaScript engine thinks that there is no block-level scope.

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

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

In the above code, the first way of writing has no curly braces, so there is no block-level scope, and let can only appear at the top of the current scope, so an error is reported. The second way of writing has braces, so block-level scope is established.

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() {}
}

// report an error
'use strict';
if (true)
  function f() {}

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.

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

The variable declared by const must not change the value, which means that once the variable is declared by const, it must be initialized immediately and cannot be left for later assignment.

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

The above code means that for const, if you just declare and not assign, an error will be reported.

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

if (true) {
  const MAX = 5;
}

MAX; // Uncaught ReferenceError: MAX is not defined

The constant declared by the const command is also not promoted, and there is also a temporary dead zone, which can only be used after the declared position.

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

The above code is called before the constant MAX is declared, and an error is reported.

The constants declared by const are also non-repeatable like let.

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

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

Nature

What const actually guarantees is not that the value of the variable cannot be changed, but that the data stored in the memory address pointed to by the variable cannot be changed. For simple types of data (numerical value, string, Boolean value), the value is stored in the memory address pointed to by the variable, so it is equivalent to a constant. But for data of composite types (mainly objects and arrays), the memory address pointed to by the variable is only a pointer to the actual data. const can only guarantee that this pointer is fixed (that is, it always points to another fixed Address), as to whether the data structure it points to is variable, it is completely uncontrollable. Therefore, you must be very careful when declaring an object as a constant.

const foo = {};

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

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

In the above code, the constant foo stores an address, which points to an object. The only thing that is immutable is this address, that is, you cannot point foo to another address, but the object itself is mutable, so you can still add new attributes to it.

Here is another example.

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

In the above code, the constant a is an array, and the array itself is writable, but if another array is assigned to a, an error will be reported.

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 new attributes does not work, and an error will be reported in strict mode.

In addition to freezing the object itself, the properties of the object should also be frozen. Below is a function to completely freeze the object.

var constantize = (obj) => {
  Object.freeze(obj);
  Object.keys(obj).forEach((key, i) => {
    if (typeof obj[key] === "object") {
      constantize(obj[key]);
    }
  });
};

ES6 six ways to declare variables

ES5 has only two ways to declare variables: var command and function command. In addition to adding the let and const commands to ES6, the following chapters will also mention two other methods of declaring variables: the import command and the class command. Therefore, ES6 has 6 ways to declare variables.

The attributes of the top-level object

The top-level object refers to the window object in the browser environment, and the global object in Node. In ES5, the properties of top-level objects 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 and the assignment of global variables are the same thing.

The properties of the top-level object are linked to global variables, which is considered to be one of the biggest design flaws of the JavaScript language. This design brings several big problems. First of all, it is impossible to report variable undeclared errors at compile time, which can only be known at runtime (because global variables may be created by the properties of the top-level object, and the properties of the Creation is dynamic); Secondly, it is easy for programmers to unknowingly create global variables (such as typing errors); Finally, the properties of top-level objects can be read and written everywhere, which is very unfavorable for modular programming. On the other hand, the window object has entity meaning, which refers to the window object of the browser, and the top-level object is an object with entity meaning, which is also inappropriate.

In order to change this, ES6 stipulates on the one hand that, in order to maintain compatibility, the global variables declared by the var command and the function command are still attributes of the top-level object; on the other hand, it stipulates that the let command and the const command , The global variables declared by the class command are not attributes of the top-level object. In other words, starting from ES6, global variables will gradually decouple from the properties of the top-level object.

var a = 1;
// If in the REPL environment of Node, it can be written as global.a
// Or use the general method and 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 a property of the top-level object; the global variable b is declared by the let command, so it is not a property of the top-level object, and returns undefined.

globalThis object

There is a top-level object in the JavaScript language, which provides a global environment (ie, global scope), and all code runs in this environment. However, the top-level objects are not uniform in various implementations.

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

In order to be able to fetch top-level objects in the same piece of code in various environments, the this keyword is now generally used, but it has limitations.

  • In the global environment, this will return the top-level object. However, this in Node.js module returns the current module, and this in ES6 module returns undefined. -this in a function, if the function is not run as a method of an object, but simply runs as a function, this will point to the top-level object. However, in strict mode, this will return undefined at this time.
  • No matter in strict mode or normal mode, new Function('return this')() will always return the global object. However, if the browser uses CSP (Content Security Policy), then the methods eval and new Function 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 methods that can barely be used.

// 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");
};

ES2020 At the level of language standards, globalThis is introduced as the top-level object. In other words, globalThis exists in any environment, and you can get the top-level object from it and point to this in the global environment.

The gasket library global-this simulates this proposal, and you can get globalThis in all environments.