JS function scope


First we need to understand the lexical scope of JS. Lexical scope: Simply defined in the lexical stage of the scope. In other words, it depends on where you scope variables and blocks when you write code (there are some ways to trick lexical scopes that are not covered here). We can think of the scope as a bubble, as shown in the following example

Bubble 1: contains the entire global scope with only one identifier: foo bubble 2: contains the scope created by foo, which has three identifiers: A, bar, and B Bubble 3: contains the scope created by bar, which has only one identifier: C

Note: bubbles are strictly contained here, not a Venn diagram that crosses boundaries. That is, no bubble can be in two outer scopes at once


What makes the new bubbles? Are only functions generating new bubbles? Can other structures in JS generate scope bubbles? Keep watching, you may have a different feeling.

Function scope: All variables belonging to this function can be used and reused across the entire function scope (nested scopes can also be used).

Example code: identifiers A, b, C, bar are all attached to foo(…) The scope of the bubble, so that it cannot be found in foo(…) External access to.

function foo(a){
	var b = 2;
    	// ...
       	function bar(){
        	// ...
        }
    	// ...
        
        var c = 3;
 }
 
 bar()  // ReferenceError
 console.log( a, b, c )	// ReferenceError
    
Copy the code

Think of the design of a module or API, which exposes the necessary content to the minimum and “hides” it all. Extends to how to choose the scope to contain variables and functions.

Such as:

function doSomething(a){
	b = a + doSomethingElse( a * 2 );
    	console.log( b * 3 );
}

function doSomethingElse(a){
	return a - 1 ;
}

var b;

doSomething(2)	// 15
Copy the code

In this code, following the minimum exposure principle, the variable b and the function doSomethingElse(…) Should be to doSomething (…). Private content implemented internally.

Improved code:

function doSomething(a){
        function doSomethingElse(a){
            return a - 1 ;
        }
        var b;
	b = a + doSomethingElse( a * 2 );
    	console.log( b * 3 );
}
doSomething(2)	// 15
Copy the code

Now let’s summarize the benefits of hiding scopes:

  • Avoid conflicts between identifiers with the same name

    Such as:

    function foo() { function(a) { i = 3; // Modify I console.log(a + 1) in the scope of the for loop; } for ( var i=0; i<10; i++ ) { bar( i * 2 ); // emmm, infinite loop}}Copy the code
  • In the global scope, when programs load third-party libraries, it is easy to cause conflicts if they do not properly handle internal variables and functions.

We now know that by adding wrapper functions around any code, we can “hide” internal variables and functions, and that nothing inside is accessible from the outer scope.

Such as:

var a = 2; Function foo() {// <-- add this line var a = 3; console.log(a); // 3 } foo(); // <-- add the line console.log(a); / / 2;Copy the code

While this technique can solve some problems, it is not ideal and leads to additional problems.

  • You must declare a named function foo(), which means that the name foo itself “pollutes” the scope
  • This line of function must be explicitly called by the function name (foo()) to run the code in it

JS provides solutions that can solve both of these problems at the same time:

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

Wrapper functions are declared with (function… Instead of just using function… Start. Functions are treated as function expressions rather than as a standard function declaration.

The important difference between a function declaration and a function expression is where their name identifier is bound. In other words (function foo(){… } as a function expression means that foo can only be used in (…) , but not external. The foo variable name is hidden within itself without contaminating the external scope.

From this we can think of anonymous functions

Anonymous functions

The most familiar scenario for function expressions is probably the callback argument, such as:

setTimeout( function() { console.log("I waited 1 second!" ); }, 1000);Copy the code

This is called an anonymous function expression. Because the function ().. There is no name identifier. Function expressions can be anonymous, and function declarations cannot omit the function name (illegal in JS).

Anonymous functions are easy and fast to write, but there are several disadvantages to consider:

  • Anonymous function tracing on the stack does not show meaningful function names, making debugging difficult.
  • Without a function name, the expired arguments.callee reference can only be used when the function needs to apply itself.
  • Code readability is omitted. A descriptive name allows the typing to speak for itself.

It is therefore a best practice to always give a function expression one

Execute the function expression immediately

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

Since the function is contained in (), it forms an expression that can be executed immediately by adding a () at the end.

This pattern is so common that the community defined a term for it: IIFE, for Immediately Invoked Function Expression.

Compared to the traditional IIFE form, many people prefer another improved mode: (function(){… } ()). If you look closely at the difference, the second () for calling is moved into the () for wrapping. These two forms are functionally identical and can be used solely by personal preference

One use of IIFE is to reverse the order of code execution. For example:

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

Block scope

We all know that ES6 introduced the let and const keywords. Did JS have block scope before ES6? The answer is yes!

with

The with keyword is used to trick the lexical scope (not recommended for now, but as a point of reference). Often used as a shortcut to repeatedly reference multiple properties of the same object.

Such as:

var obj = { a : 1, b : 2, c : 3 }; Obj obj. A = 2; obj.b = 3; obj.c = 4; With (obj){a = 3; b = 4; c = 5; }Copy the code

This does not seem to have an obvious block scope, so take a look at the following code:

function foo(obj) { with (obj) { a = 2; } } var o1 = { a : 3 }; var o2 = { b : 3 } foo( o1 ); console.log( o1.a ); // 2 foo( o2 ); console.log( o2.a ); // undefined console.log( a ); / / 2??? If you're confused here, see the explanation belowCopy the code

With can process an object that has no or more properties into a fully isolated lexical scope, so that the object’s properties are also processed in the lexical identifier. The above code can be interpreted as follows: when o1 is passed to with, the scope contains an identifier that matches the o1.a attribute. But when o2 is scoped, there is no A identifier, so the normal LHS lookup is performed. The scope of O2, foo(…) Neither the scope nor the global scope have an identifier a, so a global variable is automatically created when a = 2 is executed (in non-strict mode).

try/catch

Few people have noticed that the CATCH clause for try/catch in the JS ES3 specification creates a block scope in which declared variables are valid only within the catch

For example,

try { undefined(); // Perform an illegal operation to force an exception} catch(err) {console.log(err); // It works! } console.log( err ); // ReferenceError: err not foundCopy the code

As you can see, err only exists inside a catch, which throws an error when attempting to reference it from elsewhere.

While the block scope created by the catch clause may seem useless, a closer look reveals some useful information (to be updated later)