5. Variable promotion

Javascript assigns variables to scopes based on where and how they are declared. The behavior of a function scope is the same as that of a block scope, which can be summed up as: any variable declared in a scope will be attached to that scope. But scopes are subtly related to where variable declarations occur.

5.1 Compiler – induced variable promotion

Traditionally we think of javascript as being executed line by line from top to bottom. In fact, this is not entirely true, as shown in the following code:

a = 2;
var a;
console.log( a ); / / 2

console.log( b ); // undefined
var b = 2;
Copy the code

We know that the engine compiles JavaScript code before interpreting it. Part of the compile phase is finding all the declarations and associating them with the appropriate scope. So the right way to think about it is that all declarations, including variables and functions, are processed first before any code is executed.

As understood above, our first snippet will be processed as follows:

var a;
a = 2;
console.log( a );
Copy the code

The first part is compilation, while the second part is execution. Similarly, our second code snippet actually follows the following flow:

var b;
console.log( b );
b = 2;
Copy the code

Variable and function declarations are “moved” from where they appear in the code to the top of their scope. This process is called ascension. Only the declaration itself is promoted, while assignment or other runtime logic is left in place. It is also worth noting that each scope is promoted.

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

As shown in the code above: the declaration of the foo function (this example also includes the implied value of the actual function) is enhanced so that the call in the first line executes normally. Foo (..) The function itself internally promotes var a (not to the top of the program, obviously, but to the top of its scope).

5.2 Key points of variable promotion

Function declarations are promoted, but function expressions are not

foo(); // Not ReferenceError, but TypeError!
var foo = function bar() {    
    // ...
};
Copy the code

The variable identifier foo() in the above code is promoted and assigned to its scope (global scope in this case) because foo() does not cause ReferenceError. But foo does not assign at this point (it would if it were a function declaration rather than a function expression). Foo () raises TypeError because a function call on the value of undefined causes an illegal operation.

Function priority principle

Both function declarations and variable declarations are enhanced. However, it is important to note that functions are promoted first, and variables second. The following code:

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

It prints 1 instead of 2! The code snippet is interpreted by the engine as follows:

function foo() {    
    console.log( 1 );
}
foo(); / / 1
foo = function() {    
    console.log( 2 );
};
Copy the code

Var foo although it appears in function foo()… But it is a duplicate declaration (and therefore ignored) because function declarations are promoted to precede ordinary variables.

Duplicate declarations and overwrites

Duplicate var declarations are ignored, but function declarations that appear later can override previous ones.

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

case

function test(){
    console.log('outTest')}var aTest = 1;
(function (){
    if(false) {var aTest = 22;
        function test(){
            console.log("in")}}console.log(aTest)  // undedined
    test()  // test is not a function}) ()Copy the code

In the code above, the variable declaration in the immediate function expression is promoted to the top of the scope, i.e. the variable aTest and the function test are promoted to the top of the immediate function scope, respectively.

conclusion

Wherever a declaration in a scope appears, it is processed first before the code itself is executed. This process can be visualized as all declarations (variables and functions) are “moved” to the top of their scope. This process is called promotion. The declaration itself is promoted, but assignment operations, including assignment of function expressions, are not. Be careful to avoid duplicate declarations, especially when normal var declarations are mixed with function declarations, which can cause a lot of dangerous problems!

Six, closures

Closures are not a tool that requires learning a new syntax or pattern to use; they are a natural consequence of writing code based on lexical scope. The creation and use of closures are everywhere in your code.

6.1 Closure generation

Closures occur when a function can remember and access its lexical scope, even if the function is executed outside the current lexical scope.

Let’s look at the following code:

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

Is this a closure? Technically, maybe. But not exactly, according to the previous definition. The bar function is not executed outside of the lexical scope (foo). The most accurate way to interpret a reference to a by bar() is the lookup rules for lexical scope, and these rules are only part of the closure. (But a very important part!

In the code snippet above, the function bar() has a closure that covers the scope of Foo () (in fact, all scopes it can access, such as global scopes). You can also think of bar() as enclosed in the scope of foo(). Why is that? Because bar() is nested inside foo(). But closures defined in this way are not directly observable, nor is it possible to see how closures work in this code snippet. Lexical scopes are easy to understand, whereas closures, hidden in a mysterious shadow behind the code, are not so easy to understand. So in order to display closures more clearly, we can take a look at the following code:

function foo() {
    var a = 2;
    function bar() {         
        console.log( a );    
    }
    return bar;
}
var baz = foo();
baz(); // 2 -- That's what closures do, friend.
Copy the code

After foo() is executed, its return value (that is, the internal bar() function) is assigned to the variable baz, and baz() is called, really just calling the internal function bar() with different identifier references. Bar () clearly executes outside of its own lexical scope.

We know that engines have garbage collectors to free up memory that they no longer use. After foo() is executed, it is natural to consider recycling it because it looks like foo() ‘s content will no longer be used. The magic of closures is that they prevent this from happening. This internal scope is not reclaimed because bar() itself is in use. Bar () ‘s reference to this inner scope is called a closure.

Regardless of how the value of a function type is passed, the closure can be observed when the function is called elsewhere.

No matter how an inner function is passed outside its lexical scope, it holds a reference to the original definition scope and uses closures wherever the function is executed.

6.2 Closures in code

Callbacks and closures

Essentially, whenever and wherever you pass functions (accessing their respective lexical scopes) around as first-level value types, you’ll see closures applied to those functions. Whenever you use callbacks in timers, event listeners, Ajax requests, cross-window communication, Web Workers, or any other asynchronous (or synchronous) task, you’re actually using closures!

function wait(message) {    
    setTimeout( function timer() {        
        console.log( message );    
    }, 1000 );
}
wait( "Hello, closure!" );
Copy the code

In the example code above, an internal function (named timer) is passed to setTimeout(..) , timer has coverage of wait(..) In the scope of wait(..) After 1000 milliseconds, its internal scope does not disappear, and the timer function retains wait(..). Scope and reference to the variable message.

Loops and closures

Let’s look at a common example where we want to output the numbers 1 to 5, one per second, one at a time.

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

This code will print five 6’s at a rate of one per second when it runs. First, explain why the number 6 is present. The loop terminates if I is no longer <=5, that is, the value of I was 6 when the condition was first true.

This leads to a further question: what are the flaws in the code that cause it to behave differently than the semantics suggest? The flaw is that we try to assume that each iteration in the loop will “capture” itself a copy of I at run time. But according to how scopes work, the reality is that although the five functions in the loop are defined separately in each iteration, they are all enclosed in a shared global scope, so there is really only one I. To solve this problem,? We need more closure scopes, especially one for each iteration of the loop.

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

Considering the solution to the above problem, we use IIFE to create a new scope at each iteration. In other words, we need a block scope for each iteration. We know that the LET declaration can be used to hijack the block scope and declare a variable within the block scope. This essentially converts a block into a scope that can be closed. So the code can be improved to the following form:

for (var i=1; i<=5; i++) {
    let j = i; // Yes, block scope for closures!
    setTimeout( function timer() {        
        console.log( j );    
    }, j*1000 );
}
Copy the code

But there’s an even simpler way to write it. In the header of the for loop, the let declaration also has a special behavior. This behavior indicates that the variable is declared not just once during the loop, but every iteration. Each subsequent iteration initializes this variable with the value at the end of the previous iteration.

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

Modules and closures

Closures are also present in modules. Let’s look at the following code:

function foo() {
    var something = "cool"; 
    var another = [1.2.3];
    function doSomething() {         
        console.log( something );    
    }
    function doAnother() {        
        console.log( another.join( ! "" ")); }}Copy the code

On the surface there are no obvious closures, just two private data variables something and another, and two internal functions doSomething() and doAnother(), whose lexical scope (which is the closure) is the internal scope of foo(). Let’s look at this code:

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

This pattern is called a module in JavaScript. The most common way to implement a module pattern is often referred to as module exposure. 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 (since functions are objects, they can have attributes themselves)

For a simpler description, the module pattern needs to have two requirements.

  • There must be an external enclosing function that must be called at least once (each call creates a new module instance).
  • Enclosing functions must return at least one inner function so that the inner function can form a closure in the private scope and can access or modify the private state.

An object with function attributes is not really a module in itself. From a convenience point of view, an object returned from a function call with only data properties and no closure functions is not really a module.

CoolModule() in the example code above is called a stand-alone module creator that can be called any number of times, creating a new module instance with each call. When only one instance is needed, a simple refinement of the pattern can be made to implement the singleton pattern:

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

Now the module mechanism

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 }; }) (); 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 }; });Copy the code

Future module mechanism

Function-based modules are not a consistently recognized pattern (not recognized by the compiler), and their API semantics are taken into account only at run time. A module’s API can therefore be modified at run time (see the previous discussion of public apis). In contrast, the ES6 module API is more stable (the API does not change at run time). ES6 has added a level 1 syntax support for modules. But when loaded through the module system, ES6 treats the file as a separate module. Each module can import other modules or specific API members, as well as export its own API members.

//bar.js
function hello(who) {
    return "Let me introduce: " + who;
}
export hello;
foo.js
// Import hello() only from the "bar" module
import hello from "bar";
var hungry = "hippo";
function awesome() {    
    console.log( hello( hungry ).toUpperCase() );
}
export awesome;
baz.js
// Import complete "foo" and "bar" modules
module foo from "foo";
module bar from "bar";

console.log(  bar.hello( "rhino"));// Let me introduce: rhinofoo.awesome(); // LET ME INTRODUCE: HIPPO
Copy the code

7. Other Concepts

7.1 Dynamic scope and lexical scope

Dynamic scoping means that the scope of a function is determined at the time the function is called. Dynamic scopes do not care how or where functions and scopes are declared, only where they are called from. In other words, the scope chain is based on the call stack, not the scope nesting in the code.

Lexical scope, also known as static scope, means that the scope of a function is determined when the function is defined.

The following code results:

var value = 1;
function foo() {
    console.log(value);
}
function bar() {
    var value = 2;
    foo();
}
bar();
Copy the code

If JavaScript is static scoped, analyze the execution:

Foo () looks inside foo to see if there is a local variable value. If there is no local variable value, it looks up the previous layer of code based on where it was written, value equals 1, so it prints 1.

Assuming JavaScript is dynamically scoped, analyze the execution process:

If foo() is executed, it still looks for local value from within foo, and if it doesn’t, it looks for value from within the scope of the calling function, bar(), so it prints 2.

JavaScript uses lexical scope, so this example results in 1

7.2 Implicit and explicit scopes

To understand these two concepts, let’s look at an old problem:

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

The code above actually outputs five sixes. Because of the queue of the delay timer, even if the first delay function is executed after 1000ms, the I value is the entire for loop. The core of this problem is to retain the I value for each loop, so you want the desired output 1,2,3,4,5. We need to consider how to retain the problem.

The first:

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

The second:

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

The third:

for (var i=1; i<=5; i++) {
    let j = i; // Yes, block scope for closures!
    setTimeout(function timer() {
        console.log( j );
    }, j*1000 );
}    

Copy the code

Fourth:

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

The above methods, the first and second, belong to implicit scope. No new scope is generated, but they implicitly hijack the existing scope and retain the fleeting I value.

The third and fourth methods take advantage of the let declaration, which creates and binds a display scope. Explicit scopes are not only more prominent, they are also more robust when code is refactoring. Syntactically, this results in cleaner code by forcing all variable declarations to the top of the block. This makes it easier to determine whether a variable belongs to a scope.


If you have any questions, welcome to discuss, if you are satisfied, please manually click “like”, thank you! 🙏

For more postures, please pay attention!!