ES6 Variable scoping and enhancement: A detailed explanation of the life cycle of variables is part of my article series on modern JavaScript development: Syntactic basics and Practical tips. This article discusses in detail JavaScript scope, execution context, performance of variable promotion and function promotion under different scopes, top-level objects, and how to avoid creating global objects. I recommend reading ES6 variable declarations and assignments above.

Variable scope and promotion

Before ES6, only function scopes existed in JavaScript; In ES6, JavaScript introduced let, const and other variables to declare keyword and block-level scope, and the performance of variables and functions is inconsistent under different scope. In JavaScript, all binding declarations are initialized when the control flow reaches the scope in which they appear; Each Execution Context is divided into two phases: Memory Creation Phase and Execution Phase. During the memory allocation phase of the execution context, variable creation takes place, that is, the life cycle of the variable begins. The life cycle of a variable includes Declaration phase, Initialization phase and Assignment phase.

The traditional var keyword allows variables to be used before the declaration, when the variable is assigned undefined. Functions declared in function scope can also be used before the declaration, and the function body is also promoted to the header. This property performance is known as ascending (or ascending); Variables declared with the let and const keywords in ES6 are also initialized in the scope header, but they are only allowed after the actual declaration. The area between the scope head and where the variable is actually declared is known as the Temporal Dead Zone, and TDZ can avoid the potential problems of traditional promotion. On the other hand, since ES6 introduces block-level scopes, functions declared in block-level scopes are promoted to the head of the scope, allowing them to be used before the actual declaration. In some implementations, the function is also promoted to the head of the function scope, but is assigned undefined.

scope

Scope refers to the accessible area of variables, functions or objects during code execution. Scope determines the visibility of variables or other resources. One of the basic principles of computer security is that users should only access the resources they need, and scope is used in programming to ensure that code is secure. Beyond that, scopes can help improve code performance, track down bugs, and fix them. Scope in JavaScript is mainly divided into Global Scope and Local Scope. In ES5, variables defined inside a function belong to a Local Scope, while variables defined outside the function belong to the Global Scope.

Global scope

When we start writing JavaScript in the browser console or node.js interactive terminal, we enter what is called a global scope:

// the scope is by default global
var name = 'Hammad';Copy the code

Variables defined in a global scope can be accessed by any other scope:

var name = 'Hammad';

console.log(name); // logs 'Hammad'

function logName() {
    console.log(name); // 'name' is accessible here and everywhere else
}

logName(); // logs 'Hammad'Copy the code

Function scope

Variables defined within a function belong to the scope of the current function, and a new context is created in each function call. In other words, we can define variables with the same name in different functions that are bound to their respective function scopes:

// Global Scope
function someFunction() {
    // Local Scope # 1
    function someOtherFunction() {
        // Local Scope # 2
    }
}

// Global Scope
function anotherFunction() {
    // Local Scope # 3
}
// Global ScopeCopy the code

The drawback of function scopes is that they are too granular and cause variable passing exceptions when using closures or other features:

var callbacks = []; // Here I is promoted to the head of the current function scopefor (var i = 0; i <= 2; i++) {
    callbacks[i] = function () {
            returni * 2; }; } console.log(callbacks[0]()); //6 console.log(callbacks[1]()); //6 console.log(callbacks[2]()); / / 6Copy the code

Block-level scope

Loop bodies such as if, switch conditional selection, or for, while are so-called block-level scopes; In ES5, to implement block-level scope, you need to wrap a layer over the original function scope, that is, manually set a variable to replace the original global variable where the variable promotion needs to be restricted, for example:

var callbacks = [];
for (var i = 0; i <= 2; i++) {
    (function(I) {// the I here belongs only to the function scope callbacks[I] =function () {
            return i * 2;
        };
    })(i);
}
callbacks[0]() === 0;
callbacks[1]() === 2;
callbacks[2]() === 4;Copy the code

In ES6, you can do this directly with the let keyword:

let callbacks = []
for (leti = 0; i <= 2; I ++) {// where I belongs to the current block scope callbacks[I] =function () {
        return i * 2
    }
}
callbacks[0]() === 0
callbacks[1]() === 2
callbacks[2]() === 4Copy the code

Lexical scope

Lexical scope is an important guarantee of JavaScript closure features. In jSX-based Dynamic data Binding, the author also introduced how to use the feature of lexical scope to implement dynamic data binding. In general, in programming languages, we commonly use lexical Scope and Dynamic Scope. Most programming languages use lexical Scope. Lexical scopes focus on what is called write-time, or programming-time context, while dynamic scopes, and the common use of this, are run-time, or runtime context. Lexical scope focuses on where functions are defined, while dynamic scope focuses on where functions are called. JavaScript is typically a lex-scoped language, where a symbol refers to where the symbol name appears in the context, and local variables are lex-scoped by default. A comparison of the two can be seen in the following example:

function foo() { console.log( a ); / / 2in Lexical Scope ,But 3 in Dynamic Scope
}

function bar() {
    var a = 3;
    foo();
}

var a = 2;

bar();Copy the code

Execution context and promotion

Scope and Context are often used to describe the same concept, but Context is more concerned with the use of this in code, whereas Scope is related to the visibility of variables. The Execution Context in the JavaScript specification describes the scope of a variable. As we all know, JavaScript is a single-threaded language. Only one task is executing at a time, and other tasks are pushed into the execution context queue (read more about the Event Loop mechanism and its practical application). Each function call creates a new context and adds it to the execution context queue.

Execution context

Each Execution context is divided into a Creation Phase and a Code Execution Phase. In the creation step, Variable objects are created, the scope chain is created, and the this Object in the current context is set. A Variable Object, also known as an Activation Object, contains all variables, functions, and definitions in specific branches of the current execution context. When a function is executed, the interpreter scans all function arguments, variables, and other declarations:

'variableObject': {
    // contains function arguments, inner variable and function declarations
}Copy the code

After the Variable Object is created, the interpreter continues to create Scope chains. Scope chains tend to point to their side effect domain and are often used to resolve variables. When a specific variable needs to be parsed, the JavaScript interpreter recursively looks up the scope chain until it finds the appropriate variable or any other resource it needs. A scope chain can be thought of as an Object containing its own Variable Object references and all of its parent Variable Object references:

'scopeChain': {
    // contains its own variable object and other variable objects of the parent execution contexts
}Copy the code

An execution context can be expressed as an abstract object:

executionContextObject = {
    'scopeChain': {}, // contains its own variableObject and other variableObject of the parent execution contexts
    'variableObject': {}, // contains function arguments, inner variable and function declarations
    'this': valueOfThis
}Copy the code

Life cycle and promotion of variables

The life cycle of a variable includes three steps: Declaration Phase, Initialization Phase and Assignment Phase. The declaration step registers the variable in scope, the initialization step allocates memory for the variable and creates the scope binding, where the variable is initialized to undefined, and the final allocation step assigns the value specified by the developer to the variable. The traditional life cycle of a variable declared using the var keyword is as follows:

The let keyword declares the variable life cycle as follows:

As mentioned above, we can access a variable or function before it is defined, in what is called variable promotion (as of 1997). Variables declared by the traditional var keyword are promoted to the scope header and assigned the value undefined:

// var hoisting num; // => undefined var num; num = 10; num; / / / / = > 10functionhoisting getPi; / / = >function getPi() {... } getPi(); / / = > 3.14function getPi() {  
  return 3.14;
}Copy the code

Variable promotion only applies to variables declared with the var command. If a variable is not declared with the var command, no variable promotion occurs.

console.log(b);
b = 1;Copy the code

ReferenceError: b is not defined, that is, the variable b is not declared. This is because b is not declared with the var command, and the JavaScript engine does not promote it, but only as an assignment to the b property of the top-level object. ES6 introduces block-level scope. Variables declared with let in block-level scope are also promoted, but are not allowed to be used before the actual statement declaration:

> let x = x;
ReferenceError: x is not defined
    at repl:1:9
    at ContextifyScript.Script.runInThisContext (vm.js:44:33)
    at REPLServer.defaultEval (repl.js:239:29)
    at bound (domain.js:301:14)
    at REPLServer.runBound [as eval] (domain.js:314:12) at REPLServer.onLine (repl.js:433:10) at emitOne (events.js:120:20) at REPLServer.emit (events.js:210:7) at REPLServer.Interface._onLine (readline.js:278:10) at REPLServer.Interface._line (readline.js:625:8)  >let x = 1;
SyntaxError: Identifier 'x' has already been declaredCopy the code

Function lifecycle and promotion

The underlying function promotion also raises the declaration to the head of the scope, but unlike variable promotion, the function also raises its body definition to the head; Such as:

function b() {  
   a = 10;  
   return;  
   function a() {}}Copy the code

Is changed by the compiler to the following mode:

function b() {
  function a() {}
  a = 10;
  return;
}Copy the code

In the memory creation step, the JavaScript interpreter recognizes the function declaration through the function keyword and promotes it to the header; The life cycle of a function is simpler, with declaration, initialization, and assignment all elevated to the scope header:

If we declare a function of the same name repeatedly in scope, the former is overwritten by the latter:

sayHello();

function sayHello () {
    function hello () {
        console.log('Hello! ');
    }

    hello();

    function hello () {
        console.log('Hey! ');
    }
}

// Hey!Copy the code

In JavaScript, there are two ways to create functions: Function Declaration and Function Expression. A function declaration begins with the function keyword and follows the function name and function body. Function expressions, on the other hand, declare the function name and then assign an anonymous function to it. A typical function expression looks like this:

var sayHello = function() {
  console.log('Hello! ');
};

sayHello();

// Hello!Copy the code

Function expressions follow the rules of variable promotion, and the function body is not promoted to the scope header:

sayHello();

function sayHello () {
    function hello () {
        console.log('Hello! ');
    }

    hello();

    var hello = function () {
        console.log('Hey! ');
    }
}

// Hello!Copy the code

In ES5, you are not allowed to create functions in block-level scopes; ES6 allows functions to be created in block-level scopes. Functions created in block-level scopes are also promoted to the current block-level and function scope headers. The difference is that the function body is no longer promoted to the function scope head, but only to the block-level scope head:

f; // Uncaught ReferenceError: f is not defined
(function () {
  f; // undefined
  x; // Uncaught ReferenceError: x is not defined
  if (true) {
    f();
    let x;
    function f() { console.log('I am function! '); }}} ());Copy the code

Avoid global variables

In computer programming, a global variable is a variable that is accessible in all scopes. Global variables are a bad practice because they can cause problems such as an existing method and overwriting global variables, and when we don’t know where the variables are defined, the code becomes difficult to understand and maintain. In ES6 you can use the let keyword to declare local variables; good JavaScript code does not define global variables. In JavaScript, we sometimes create global variables by accident, meaning that if we forget to declare a variable before using it, it is automatically assumed to be a global variable, as in:

function sayHello(){
  hello = "Hello World";
  return hello;
}
sayHello();
console.log(hello);Copy the code

In the code above, because we didn’t declare hello when we used the sayHello function, it was created as some global variable. If we want to avoid this accidental global variable creation error, we can forbid global variable creation by enforcing strict mode.

The function package

To avoid global variables, the first thing to do is to ensure that all code is wrapped in functions. The simplest way to do this is to put all the code directly into a function:

(function(win) {
    "use strict"; // Further avoid creating a global variable var doc = window.document; // Declare your variable here // Some other code}(window));Copy the code

Declaring a namespace

var MyApp = {
    namespace: function(ns) {
        var parts = ns.split("."),
            object = this, i, len;
        for(i = 0, len = parts.lenght; i < len; i ++) {
            if(! object[parts[i]]) { object[parts[i]] = {}; } object = object[parts[i]]; }returnobject; }};// Define the namespace
MyApp.namespace("Helpers.Parsing");

// You can now use the namespace
MyApp.Helpers.Parsing.DateParser = function() {
    // Do something
};Copy the code

modular

Another technique used by developers to avoid global variables is encapsulation in the Module Module. A module is a generic function that does not require the creation of new global variables or namespaces. Don’t put all your code in a function that performs a task or publishes an interface. This section uses Asynchronous Module Definition (AMD) as an example. For more detailed knowledge about JavaScript modularity, refer to a brief history of JavaScript Module evolution

/ / define
define( "parsing".// Module name
        [ "dependency1"."dependency2"].// Module dependency
        function( dependency1, dependency2) { // Factory method

            // Instead of creating a namespace AMD modules
            // are expected to return their public interface
            var Parsing = {};
            Parsing.DateParser = function() {
              //do something
            };
            returnParsing; });// Load the module from require.js
require(["parsing"], function(Parsing) {
    Parsing.DateParser(); // Use the module
});Copy the code