This article is taken from my public account [Sun Wukong, Don’t talk nonsense]

In JavaScript, scopes seem to be the simplest. But for me, sometimes the results are confusing. Especially when it comes to “closures,” I feel a little uneasy. I wanted to use this article to thoroughly understand everything about “JavaScript scope.”

Definition of scope

First of all, what is the scope? Let’s start with the basic features of a programming language.

One of the most basic features of almost any programming language is the ability to store a value in a variable and then access and modify that value later. In fact, it is this ability to store and access the value of a variable that brings state to the program. Without the concept of state, programs can perform simple tasks, but they are highly limited.

Introducing programs into programs raises several interesting questions: where are these variables stored? More importantly, how do you find them when your program needs them?

These problems illustrate the need for a well-designed set of rules to store variables and then rely on that rule to find them. This set of rules is called scope.

Note that the actual place to store variables is either stack memory or heap memory, which many people would think of as scope. They are where variables are stored, and these cannot be understood as scopes. The specific rules are the scope.

Oh, scopes are a set of rules for finding names or variables. So where will it be used? It’s in JavaScript engines. Read more about JavaScript engines in this article.

So, given the rules, let’s see what the rules look like?

Scopes are nested

In practice, you often need to take care of several scopes simultaneously. A common one is nesting of scopes.

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

Consider the code:

functionfoo(a){ console.log(a + b); } var b=2; foo(18); / / 20Copy the code

A reference to variable B cannot be done inside Foo, but can be found in the upper scope.

Such layers of nested processes can be simply understood as a chain of scopes.

Dynamic and static scopes

There are two main working models for scopes.

The first is the most common lexical scope adopted by most programming languages. The scope of this method is defined at the lexical stage. In short, the scope of this method is determined by where you write the variable and block scopes when writing the code, so the lexical analyzer will keep the scope unchanged when processing the code.

The second is dynamic scoping, typified by the Bash language. This model makes scopes a form that is dynamically determined at run time, that is, it doesn’t care how and where functions and scopes are declared, only where they are called from. In other words, its scope chain is based on the call stack, not the scope nesting in the code.

To illustrate the difference, let’s look at an example:

The first is the lexical scope for JavaScript code:

/* Foo (); /* foo (); /* foo ();function foo() {
    console.log(value); // 1
}

function bar() {
    var value = 2;
    foo();
}

bar();
Copy the code

Then there is the dynamic scope represented by bash:

/* If foo is executed, it still looks inside foo to see if there is a local variable value. If there is no local variable value, it looks inside the scope of the calling function, bar, so it prints 2 */ value=1function foo () {
    echo $value; // 2
}
function bar () {
    local value=2;
    foo;
}
bar
Copy the code

Highlight: In JavaScript, no matter where a function is called or when it is called, its lexical scope is determined only by the position at which the function is declared.

Shadowing effect of scope

Once we understand the use of lexical scope, we can see the shadowing effect: the scope lookup stops when the first matching identifier is found. It is possible to define identifiers of the same name in multiple nested scopes, which is called the “shadowing effect” (an internal identifier “overshadows” an external identifier).

This masking effect is also a common test topic. When you understand it, it’s easy.

Who generates the new scope

We’ve covered scope in some detail. Further, we want to know, in JavaScript, which operations can generate new scopes? Will only functions generate new scopes? What other structures or operations can create scopes?

Function scope

As we imagine, each function declared creates a scope for itself. Let’s take an example to understand:

function foo(a){
    var b = 2;
    
    function bar() {... } / /... more code here var c = 3; }Copy the code

In this code snippet, foo() has the identifiers A, B, c, bar in scope. Bar () has its own scope. The global scope contains only one identifier: foo.

Because identifiers A, B, c, and bar are all scoped by foo(), they cannot be accessed from outside foo(). That is, these identifiers cannot be accessed from the global scope.

This limitation on function scope is especially useful when we want to encapsulate some function code. By creating a function scope, you hide the internal implementation.

Why hide the internal implementation? Think of it in terms of the principle of least privilege or the principle of least exposure from a software design perspective: expose only the bare minimum and “hide” everything else.

Block scope

If you look at more mainstream programming languages than JS, you’ll find that block scope is a common concept. Unfortunately, the usual block-scoped notation (yes, that very simple curly bracket notation) is not available in JS. (Yes, there isn’t. Not surprisingly, JS was designed by author Brendan Eich in just 10 days.

So you can see how dangerous it is to write:

// Since there is no block scope here, the variable I is actually hung in the outer scope (in this case, the global scope)for(var i = 0; i< 5; i++){
    console.log("the i is : ", i);
}
Copy the code

try/catch

However, while common block scoping syntax does not exist, there is a very obscure specification in JS syntax (in effect since ES3) : the catch clause in try/catch creates a block scope in which declarations are only valid within the catch.

try { undefined(); // Force an Error} catch(err){console.log(err); } console.log(err); //ReferenceError : err not foundCopy the code

After seeing this, can you find it very helpless? You think: what’s the use of this? Who would write such ugly code? Just to use block scope…

But it does have applications. As we know today (early 2020), the let keyword in ES6 syntax creates block scope and implements functionality we’ve always wanted. But how do we switch the let keyword syntax to make it work in environments below ES6? (I.e. degraded service)

In general, tools like Babel help us convert ES6 code into ES5 code, and we don’t care how they are implemented, right? I can now tell you that writing it as a try/catch is the preferred way to migrate most functionality in ES6 to generate ES5-compatible code.

ES6 new keyword: let/const

To address this flaw in JavaScript design, the ES6 syntax specification defines two new keywords: let/const. They provide two declarations in addition to var, both of which can be used to create block-scoped variables.

Focus on the let keyword, which binds variables to any scope they are in (usually {.. } inside). In other words, let implicitly hijacks its scope for the variables it declares. Here’s a chestnut to illustrate:

var foo = true;

if(foo){
    let bar = foo *2;
    console.log( bar );
}
console.log( bar ); // RefenceError
Copy the code

As you can see, because of the let declaration, the {} of the conditional statement is extended to a new scope, and the variables in that scope are no longer accessible to the external scope. Note, however, that this approach implicitly creates a new scope.

With let, we can easily implement the block-scoped functionality we want, explicitly:

function bar() {

    var b = 2;
    
    {
        let a = 10;
        console.log(a);
    }
    
    console.log( b ); // 2
    console.log( a ); // ReferenceError
}

Copy the code

It is important to note, however, that declarations using lets do not promote in block scope. For ascension, see the following chapters.

The const keyword, on the other hand, has a similar effect to let. However, a variable declared by const is a constant and is not allowed to be modified by the program.

ascension

One question that is often asked in scope is about promotion. There are two types of promotion: promotion of variable declaration and promotion of function declaration. Let’s look at it separately:

Enhancement of variable declarations

Let’s look at a chestnut:

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

Var a = 2; var a = 2; var a = 2; However, the actual output is 2.

When the engine sees var a = 1; when the engine sees var a = 1; It treats them as two declarations: var a and a = 1. Where the first declaration is made at compile time, the second assignment declaration is left in place for execution. So JavaScript will do something like this with our chestnut code:

var a; // compile phase a = 2; // Console. log(a); // Execution phaseCopy the code

This practice of “moving” definition declarations (or function declarations) from their place in the code to the top is called promotion. So, do we really understand this concept? Here’s another example:

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

At this point, what’s going to be output? The answer is undefined. JavaScript is processed in this order:

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

It is important to note, however, that the so-called promotion is only promotion within a specific scope. The specific scope is where the restriction rules for ascension lie.

Enhancement of function declarations

Promotion of function declarations is similar to promotion of variables. Let’s look at chestnuts:

foo();

function foo(){
    console.log("foo");
}
Copy the code

This can be output normally.

But there are two things worth noting about function declarations:

  • Function declarations are promoted, but function expressions are not.
  • Function declarations and variable declarations are promoted, but functions are promoted before variables.

Let’s talk about chestnuts:

foo(); // Instead of ReferenceError, instead of TypeError var foo =function(){
    console.log("foo");
}

Copy the code

The JavaScript engine at this point looks at the code like this:

var foo; foo(); // Foo is defined, so it will not ReferenceError, but foo's value is undefined, and a call to () on it will report TypeError foo =function(){
    console.log("foo");
}
Copy the code

I think it’s easier to understand.

closure

Once we understand lexical scope, once we understand scope chains, we now look at closures, and it’s not so bottomless.

Here’s a straightforward definition of closures:

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

Here’s how to understand the passage:

function foo(){
    var a = 2;
    function bar(){
        console.log(a);
    }
    returnbar; } var baz = foo(); baz(); // 2 -- This is the closureCopy the code

We look at the code from a scoped perspective: the variable a is in the lexical scope of the function foo. Because of the nested scope chain, the bar function can access variable A. But the outermost scope cannot access it (variable A).

The code returns the bar function to the outermost scope, but it still remembers the lexical scope it has access to, regardless of where it was executed.

That’s the closure. As long as the core concept of scope is grasped, everything will be the same. Closures are not that difficult.

Let’s use a slightly more complicated closure:

function foo(){
    var a = 3;
    function baz(){
        console.log(a);
    }
    bar(baz);
}

function bar(fn){
    fn();
}

foo();
Copy the code

This chestnut includes function promotions and closures. But with a little thought, you get the answer


JavaScript in-depth series:

“var a=1;” What’s going on in JS?

Why does 24. ToString report an error?

Here’s everything you need to know about “JavaScript scope.

There are a lot of things you don’t know about “this” in JS

JavaScript is an object-oriented language. Who is for it and who is against it?

Deep and shallow copy in JavaScript

JavaScript and the Event Loop

From Iterator to Async/Await

Explore the detailed implementation of JavaScript Promises