Original article: wangkaidong

1. Scope-related concepts

1.1 Compilation Principles

1.1.1 Compilation process of traditional languages
  1. Word Segmentation/Lexical Analysis (Tokenizing/Lexing)

    This process breaks down a string of characters into blocks of code that make sense (to the programming language), called lexical units (tokens). For example, consider programsvar a = 2; . The program is usually broken down into the following lexical units:var,a,=,2;. Whether Spaces are considered lexical units depends on whether they make sense in the language.

The distinction between tokenizing and Lexing is subtle and obscure, with the main difference being whether the identification of lexical units is done statically or stateless. Simply put, if a lexical unit generator invokes stateful parsing rules to determine whether A is an independent lexical unit or part of another lexical unit, the process is called lexical analysis.

  1. The process is to convert a stream of lexical units (arrays) into a tree of nested elements that represents the syntax of the program. This Tree is called the Abstract Syntax Tree (AST).

  2. Code generation transforms the AST into executable code.

  3. The difference between the JavaScript

    1. JavaScript engines do not have as much time to optimize as compilers in other languages do, because unlike other languages, the compilation process for JavaScript does not take place prior to build.
    2. With JavaScript, most of the time compilation happens a few microseconds (or less!) before the code executes. Time. Behind the scope we will discuss, JavaScript engines use various methods (such as JIT, which can delay compilation or even recompile) to ensure optimal performance.
    3. In short,Any snippet of JavaScript code is compiled before execution (usually just before execution). Therefore, the JavaScript compiler first checks thevar a = 2; The program compiles, is ready to execute, and usually executes immediately.

1.2 Understanding of scope

1.2.1 tovar a = 2The processing of
  1. When var a is encountered, the compiler asks the scope if a variable of that name already exists in the collection of the same scope. If so, the compiler ignores the declaration and continues compiling. Otherwise it will require the scope to declare a new variable in the collection of the current scope and name it a.

  2. The compiler then generates run-time code for the engine to handle the assignment of a = 2. The engine starts by asking the scope if there is a variable called A in the current set of scopes. If not, the engine uses this variable; If not, the engine continues to look for the variable

    Assignment to a variable performs two actions: first, the compiler declares a variable in the current scope (if it hasn’t been declared before), and then at runtime the engine looks for the variable in the scope and assigns it if it can find it.

1.2.2 Two search engines

The compiler generates code in the second step of the compilation process, and when the engine executes it, it looks for the variable A to determine if it has been declared. The lookup process is assisted by the scope, but how the engine performs the lookup affects the final result.

  1. The LHS query attempts to find the variable’s container itself
a = 2;
Copy the code

The reference here is an LHS reference that finds the target for the = 2 assignment

  1. RHS queries basically look up the value of a variable, meaning “get the value of XX”
console.log(a);
Copy the code

This is an RHS reference, and a is not assigned, so we need to find and obtain the value of a

> can be understood as "who is the target of the assignment operation (LHS)" and "who is the source of the assignment operation (RHS)".Copy the code

The JavaScript engine first compiles the code before it executes, during which declarations like var a = 2 are broken down into two separate steps:

  1. First, var a declares new variables in its scope. This happens at the very beginning, before the code executes.
  2. Next, variable A = 2 is queried (LHS query) and assigned to it.

1.3 Scope nesting

Nesting of scopes occurs when a block or function is nested within another block or function. Therefore, when a variable cannot be found in the current scope, the engine will continue searching in the outer nested scope until it finds the variable or reaches the outermost scope (that is, the global scope).


1.4 abnormal

Why is it important to distinguish BETWEEN LHS and RHS? Because the two queries behave differently when the variable has not yet been declared (it cannot be found in any scope)

function foo(a) {
    console.log( a + b );
    b = a;
}
foo( 2 );
Copy the code
  1. For the first timebThis variable cannot be found in an RHS query. That is, this is an “undeclared” variable because it cannot be found in any relevant scope. If the RHS query does not find the desired variable in all nested scopes, the engine throws itReferenceErrorThe exception. It’s worth noting that,ReferenceErrorIs a very important exception type.
  2. When the engine executes an LHS query, if the target variable cannot be found at the top level (global scope), the global scope creates a variable with that name and returns it to the engine, provided the program is not running in “strict mode.”

Lexical scope

Scope is one of the main working models adopted by most programming languages.

2.1 concept

  1. Lexical scope is the scope of the definition at the lexical stage
  2. The lexical scope is determined by where you write the variable and block scopes when you write code, so the lexical analyzer keeps the scope the same when it processes the code (most of the time).

Lexical scope means that the scope is determined by the position of the function declaration when the code is written. The lexical analysis phase of compilation basically knows where and how all identifiers are declared, and thus can predict how they will be looked up during execution.

2.1.1 lookup

The scoped lookup stops when the first matching identifier is found. You can define identifiers with the same name in multiple nested scopes. This is called ** “masking effect” ** (internal identifiers “masking” external identifiers). Regardless of the shadowing effect, scoping searches always start at the innermost scope of the runtime and work up and out until the first matching identifier is found.

Global variables automatically become properties of global objects (such as window objects in the browser), so they can be accessed indirectly, not directly by their lexical names, but by references to global object properties.

window.a
Copy the code

This technique allows access to global variables that are masked by the same name. But non-global variables that are obscured cannot be accessed anyway.

2.1.2 Cheat morphology

Cheating lexical scope can lead to performance degradation

  1. eval eval(..)A function can take a string as an argument and treat its contents as if they existed at that point in the program at the time of writing. In other words, you can programmatically generate code in the code you write and run it as if it were written in that location.
  2. with withOften used as a shortcut to repeatedly reference multiple properties in the same object, without the need to repeatedly reference the object itself.
var obj = {
    a: 1.b: 2.c: 3
};
// 单调乏味的重复"obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// Simple shortcut
with (obj) {
    a = 3;
    b = 4;
    c = 5;
}
Copy the code

3. Function scope and block scope

3.1 Scope in functions

meaning

All variables belonging to this function can be used and reused throughout the scope of the function (and in fact within nested scopes).

3.1.1 Hiding internal implementations

concept

  1. Taking an arbitrary piece of written code and wrapping it around function declarations is essentially “hiding” the code.
  2. The actual result is that a scope bubble is created around the code snippet, meaning that any declarations (variables or functions) in the code will be bound to the scope of the newly created wrapper function, not the previous one. In other words, you can wrap variables and functions in the scope of a function, and then use that scope to “hide” them.

The principle of least privilege In software design, you should expose as little as necessary and “hide” everything else, such as the API design of a module or object.

Another benefit of conflict-avoidance hiding is that variables and functions in the “scope” can avoid conflicts between identifiers of the same name, two identifiers that may have the same name but serve different purposes, which can inadvertently cause naming conflicts. Collisions can cause the value of a variable to be accidentally overwritten.

application

  1. Global namespaces When multiple third-party libraries are loaded into a program, conflicts can easily arise if they do not properly hide internally private functions or variables. These libraries usually declare a variable with a unique enough name in the global scope, usually an object. This object is used as the library’s namespace, and all functions that need to be exposed to the outside world become attributes of the object (namespace), rather than exposing their identifiers to the top-level lexical scope.
var MyReallyCoolLibrary = {
    awesome: "stuff".doSomething: function() {
        // ...
    },
    doAnotherThing: function() {
        // ...}};Copy the code
  1. Module management Another way to avoid collisions, similar to the modern module mechanism, is to use one of the many module managers. With these tools, any library does not need to add identifiers to the global scope, but instead imports the library’s identifiers explicitly into another specific scope through a dependency manager mechanism.

3.2 Function scope

The easiest way to distinguish a function declaration from an expression is to look at where the function keyword appears in the declaration (not just in one line of code, but in the entire declaration). If function is the first word in the declaration, it is a function declaration; otherwise, it is a function expression.

function foo() {
    var a = 3;
    console.log( a ); / / 3
}
Copy the code

As well as

(function foo(){
    var a = 3;
    console.log( a ); / / 3}) ();Copy the code

The most important difference between function declarations and function expressions is where their name identifiers will be bound. Compare the previous two code snippets. In the first fragment, foo is bound to its scope and can be called directly from foo(). In the second fragment foo is bound to a function within the function expression itself rather than its scope. In other words (function foo(){.. }) as a function expression means that foo can only be used in… Is accessed from the location represented, but not from the external scope. The foo variable name being hidden within itself means that it does not unnecessarily contaminate the external scope.

3.2.1 Anonymous Function

Disadvantages of anonymous functions:

  1. Anonymous functions do not show meaningful function names in the stack trace, making debugging difficult.
  2. If there is no function name, only an expired one can be used when the function needs to reference itselfarguments.calleeReferences, such as in recursion. Another example of a function needing to reference itself is when the event listener needs to unbind itself after the event is fired.
  3. Anonymous functions omit function names that are important to code readability/understandability. A descriptive name lets the code speak for itself.

Solution:

Inline function expressions are powerful and useful — the difference between anonymous and named doesn’t affect this at all. Assigning a function name to a function expression solves this problem effectively. It is a best practice to always name functional expressions

setTimeout( function timeoutHandler() { // <-- has a function name
    console.log( "I waited 1 second!" );
}, 1000 );
Copy the code
3.2.2 Execute function expressions immediately
var a = 2;
(function foo() {
    var a = 3;
    console.log( a ); / / 3}) ();console.log( a ); / / 2
Copy the code

Because the function is enclosed within a pair of () parentheses, it becomes an expression that can be executed immediately by adding another () to the end, such as (function foo(){.. }) (). The first () turns the function into an expression, and the second () executes the function.

Another very common advanced use is to call them as functions and pass in arguments.

var a = 2;
(function IIFE( global ) {
    var a = 3;
    console.log( a ); / / 3
    console.log( global.a ); / / 2}) (window );
console.log( a ); / / 2
Copy the code

Another variation is to invert the running order of the code, placing the function that needs to be run in the second place and passing it in as an argument after IIFE execution.

var a = 2;
(function IIFE( def ) {
    def( window); }) (function def( global ) {
    var a = 3;
    console.log( a ); / / 3
    console.log( global.a ); / / 2
});
Copy the code

4. Scope chain

A scope chain is essentially a list of Pointers to variable objects that reference but do not actually contain them.

4.1 Execution environment and active objects

When a function executes, an internal object called the execution environment/execution context is created. The execution environment defines functions and other data that variables can access. Each execution environment has an associated variable object in which all variables and functions defined in the environment are stored and accessed by the compiler as it processes data. When the execution flow enters a function, the environment of the function is pushed into an environment stack, and when the function completes, the stack ejects the environment, returning control to the previous execution environment.

  • The execution environment of a function is unique each time it is executed
  • Multiple calls to the function create the execution environment multiple times
  • After the function completes execution, the execution environment is destroyed

When code executes in an environment, a scope chain of variable objects is created to ensure orderly access to all variables and functions that the execution environment has access to. The front end of the scope is always the variable object of the current execution environment. If the environment is a function, then the variable object is its active object, which initially includes only arguments objects. The next variable object in the scope chain comes from its external environment, and so on down to the global variable environment.

4.2 [[scope]]attribute

The [[scope]] property inside a function is a dummy property that holds the scope chain of the function’s parent scope. This property corresponds to a list of objects that can only be accessed internally by JavaScript, not by syntax.

  1. Function definition

    When a function is defined in the global context, the value of[[scope]]Property contains only one global object; The function inside the function body is defined when it enters the function body execution environment[[scope]]Property contains the global object, as well as the currently active object.
  2. Function call

    According to the definition of execution environment, when a function is called, the corresponding execution environment is created, and each execution environment corresponds to a variable object.
    1. The first step is to create an active object of its ownthisArguments, definition of local variables (including named arguments), and scope chain of a variable object[[scope chain]]
    2. Then, put the implementation environment[[scope]]Copy to in order[[scope chain]]In the water.
    3. Finally, push the active object into[[scope chain]]The top, like this[[scope chain]]Is an ordered stack that preserves ordered access to all variables and objects that the execution environment has access to.
  3. Function execution
    • When an identifier is encountered, it is searched in the scope chain of the Execution Context based on the name of the identifier. Starting with the first object in the scope chain (the function’s active object), if not found, the next object in the scope chain is searched, and so on until the definition of the identifier is found.
    • As the function is executed, assignment and query operations are carried out along the scope chain step by step. After the function is completed and returned, the execution environment and scope of the function are popped up from the corresponding stack.

Function lifecycle:

4.3 No block-level scope

JavaScript does not have block-level scopes (if/else/for/while), where variables and functions defined are added to the current execution environment (function or global).

5. The closure

In combination with the content of scope chain, a closure is a function that preserves the scope chain and execution environment inside a function by assigning values to variables in the global environment, so that its active objects cannot be reclaimed by the reclamation mechanism and can access all variables and parameters defined in the scope chain.

5.1 Implementation of closures

function foo() {
    var a = 2;
    function bar() { 
        console.log( a );
    }
    return bar;
}
var baz = foo();
baz(); / / 2
Copy the code

The lexical scope of the function bar accesses the internal scope of foo and then passes the bar function object as a return value type. After foo is executed, its return value (bar function object) is assigned to the variable baz, essentially referring to the function bar that called the inner scope by a different identifier. The declared position of bar determines that it has access to foo’s internal scope, and its [[scope]] property also contains the scope chain bar->foo->window, which baz saves on assignment.

5.2 Problems solved by closures

  • Fixed value issues in loops

    for (var i = 1; i <= 5; i++) {
        setTimeout(function timer() {
            console.log(i);
        }, 1000);
    }
    Copy the code

    The output of the above example does not intuitively infer 1, 2, 3, 4, and 5, but outputs 5 for two reasons:

    1. The first issetTimeoutIn the above code, even if the waiting time is set to 0, the output result is still 5 5’s. The reason for this is thatsetTimeoutThey are pushed to a wait queue and invoked “simultaneously” and “immediately” after the execution of other waiting eventssetTimeout, its execution order is related to delay time. Therefore, the loop is executed in the order that all loop accumulative processes are executed first, and then calls are made simultaneously in the wait queuesetTimeout.
    2. Second, because JavaScript has no block-level scope, it looks like five different ones in codeiIn fact, since the runtime is in the same execution environment, it uses the same global scope, actually only onei.

    Solution:

    for (var i = 1; i <= 5; i++) {
        (function(j) {
            setTimeout(function timer() {
                console.log(j);
            }, j*1000);
        })(i);
    }
    Copy the code

    Using IIFE to declare and execute a function immediately to create a scope solves the execution delay problem of setTimeout. At the same time, I in the global execution environment is passed to each iteration function as the parameter of each round, thus solving the problem.

  • The module

    1. Creating private variables
    2. Simulate block-level scopes
  1. Implement modules using closures
function CoolModule() {
    var something = "cool"; 
    var another = [1.2.3];
    function doSomething() { 
        console.log( something );
    }
    function doAnother() {
        console.log( another.join( ! "" ")); }return {
        doSomething: doSomething, 
        doAnother: doAnother
    };
}
var foo = CoolModule(); 
foo.doSomething(); // cool
foo.doAnother(); / / 1! 2! 3
Copy the code

First, CoolModule() is just a function that must be called to create a module instance. Neither the inner scope nor the closure can be created without executing an external function. Second, CoolModule() returns a syntax using object literals {key: value,… } to represent the object. The returned object contains references to internal functions rather than internal data variables. We keep internal data variables hidden and private. You can think of the return value of this object type as essentially the module’s public API. The return value of this object type is eventually assigned to the external variable foo, which can then be used to access attribute methods in the API, such as foo.dosomething ().

It is not necessary to return an actual object from a module; you can also return an internal function directly. JQuery is a good example. The jQuery and $identifiers are the public apis of the jQuery module, but they are functions themselves (functions are also objects that can have attributes).

The doSomething() and doAnother() functions have closures that cover the internal scope of module instances (implemented by calling CoolModule()). When functions are passed out of the lexical scope by returning an object containing a property reference, conditions are created for closures to be observed and practiced. CoolModule creates a new module instance each time it is called. When the singleton pattern is needed, it can be built using IIFE enhancements:

var foo = (function CoolModule() { 
    var something = "cool";
    var another = [1.2.3];
    function doSomething() { 
        console.log( something );
    }
    function doAnother() {
        console.log( another.join( ! "" ")); }return {
        doSomething: doSomething, 
        doAnother: doAnother }; }) (); foo.doSomething();// cool 
foo.doAnother(); / / 1! 2! 3
Copy the code
  1. Most modern module dependency loaders/managers essentially encapsulate this module definition into a friendly API:
var MyModules = (function Manager() {
    var modules = {};
    function define(name, deps, impl) {
        for (var i=0; i<deps.length; i++) {
            deps[i] = modules[deps[i]];
        }
        modules[name] = impl.apply( impl, deps );
    }
    function get(name) {
        return modules[name];
    }
    return {
        define: define,
        get: get }; }) ();Copy the code

Modules [name] = impl.apply(impl, deps). Introduce wrapper functions (you can pass in any dependency) and store the return value, the module’S API, in a list of modules managed by name.

MyModules.define( "bar"[],function() {
    function hello(who) {
        return "Let me introduce: " + who;
    }
    return {
        hello: hello }; }); MyModules.define("foo"["bar"].function(bar) {
    var hungry = "hippo";
    function awesome() {
        console.log( bar.hello( hungry ).toUpperCase() );
    }
    return {
        awesome: awesome }; });var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(
    bar.hello( "hippo"));// <i>Let me introduce: hippo</i>
foo.awesome(); // LET ME INTRODUCE: HIPPO
Copy the code