JavaScript scope and closures

Speaking from how browsers compile JS code

I have been thinking for a long time, when we give the code to the browser, how the browser converts the code into a lively web page. Before the JS engine executes our code, what the browser does to our code, this process is like a black box to me, mysterious and curious.

understandingvar a = 2

Every day we write var a = 2simple JS code like this, but the browser is a machine, and it can only recognize binary 0s and 1s, which var a = 2is definitely harder for us than foreign languages. It doesn't matter if you have difficulties. At least our problem is clear now. We need to know how it can transform meaningful human characters into 0s and 1s that meet certain rules.

Think about how we read a sentence (think of a foreign language that we are not so familiar with). When we are not familiar with English, we actually prefer to understand the words one by one. These words become a Meaningful sentences. The browser is actually the case var a = 2, the browser actually see is that var, a, =, 2this is one word. This process is called the lexical analysis stage. In other words, this process breaks up a string of characters into meaningful code blocks (for the programming language).
Just as we combine words into sentences according to grammatical rules, the browser will also combine the above decomposed code blocks into a tree (AST) representing the grammatical structure of the program. This stage is called the grammatical analysis stage. It is already a meaningful foreign language, but it is one step away from its direct understanding of code generation and conversion of the code into a meaningful machine language (binary language).

We summarize the three stages of experience

- 词法分析:分解代码为有意义的词语;
* 语法分析:把有意义的词语按照语法规则组合成代表程序语法结构的树(AST);
* 代码生成:将 AST 转换为可执行代码

Through the above three stages, the browser can already run the executable code we got. There is also a collective call for the three stages to do the compilation stage. We call the execution of the executable code afterwards the running phase.

When is the scope of JS determined

In programming languages, there are generally two types of scope, lexical scope and dynamic scope. The lexical scope is the scope determined by the structure of the code written during programming. Generally speaking, after the compilation is completed, the scope has been determined, and the code will not change during the running process. And the dynamic scope listens to the name and knows that the scope will change dynamically during the code running. It is generally believed that the scope of our JavaScript is lexical scope (in general, because JavaScript provides some methods for dynamically changing the scope, which will be introduced later).

The lexical scope is the scope determined by the structure of the code written during programming. Comparing what the browser does at the compilation stage, we find that the lexical scope is determined at the compilation stage. Seeing here, did you suddenly understand why we often hear the phrase "the scope of a function is determined at the stage of function definition". Next, we will explain what rules the function scope is determined by.

Scope in JS

What is the scope?

What is scope? "You don't know js" gives such a concept:

Use a strict set of rules to distinguish which identifiers have access to those grammars.

Okay, so abstract, what is the identifier? How to understand the scope? Let's look at them one by one.

Identifier:

We know that when our program runs, our data ("string", "object", "function", etc. are all loaded into memory). So how do we access the corresponding memory area, the identifier will work at this time, through which we can find the corresponding data, from this perspective, variable names, function names, etc. are identifiers.

The operation of
the identifier knows the identifier, let's think about what operations we usually perform on the identifier. In fact, there are no more than two types. Look at the following code:

// 第一种定义了标识符`a`并把数值2赋值给了`a`这种操作有一个专门的术语叫做`LHS`
var a = 2;

// 第二种,var b = a ,其实对应a ,b 两个操作符是不同的操作,对b来说是一个赋值操作,这是LHS,但是对a来说却是取到a对应的值,这种操作也有一个专门的术语叫做“RHS”
var b = a;

To summarize, there are the following two operations for identifiers

- 赋值操作(LHS);常见的是函数定义,函数传参,变量赋值等等
* 取值操作(RHS);常见包括函数调用,
Look back at scope

Knowing the identifier and the two operations on the identifier, we can easily understand the scope. The scope actually defines the scope of our identifier operation at runtime, corresponding to the actual problem. It is where we can call functions or variables that we are familiar with.

Scope can also be seen as a set of rules for finding variables based on names. Then let's take a closer look at this rule. When a variable cannot be found in the current scope, the engine will continue to search in the outer nested scope until it finds the variable or reaches the outermost scope (also Is the global scope).

The term nesting is mentioned here. Let's look at the factors in js that can form a scope.

Scope types in JS

Function scope

Function scope is the most common scope in js. Function scope gives us the most intuitive experience that internal functions can call variables in external functions. Layers of functions intuitively form nested scopes. But I'm really sorry for saying this only. I still remember what we often hear "if we assign an undefined variable inside a function, this variable will be transformed into a global variable". For me, this sentence was almost memorized, and I have never been able to understand it. We understand this sentence from the perspective of the operation of the identifier.

var a = 1;

function foo(){ // b第一次出现在函数foo中 b = a ; } foo(); // 全局可以访问到b console.log(b); //1

When we call foo(), we actually perform LHS operation on b (take the value of a and assign it to b), there is no var let in front of b, so the browser first foo()looks for the identifier b in the scope, and the result is in b I did n’t find it. Install the scope rule. The browser will continue to foo()search for the identifier b in the outer scope. The result is still not found, indicating that there is no defined b in the scope of the query identifier b. In non-strict mode, the LHS operation will define a b at the outermost level (that is, the global) of the searchable range, so b will become a global variable (strict mode LHS cannot find ReferenceError). So that sentence can be understood. It is also worth noting that RHS operations on operators will have different situations. Whether strict or non-strict mode RHS cannot be found, return a ReferenceError error (unreasonable operations on the value found by RHS will return an error TypeError(scope discrimination Successful operation illegal.)).

Closures: Closures are the natural result of writing code based on lexical scope. You do n’t even need to consciously create closures in order to use them. The creation and use of closures are everywhere in your code. What you are missing is a thinking environment that recognizes, embraces and influences closures according to your own wishes.

Block scope

In addition to function scope, JS also provides block scope. We should be clear that scope is for identifiers, and block scope limits identifiers {}to.

ES6 provided let, consta method identifiers are fixed to the declaration block. We often ignore try/catchthe catchstatement will create a block scope.

Method of changing function scope

Generally speaking, the lexical scope has been determined during the code compilation stage. This certainty is actually very beneficial. During the execution of the code, it can predict how to find them during the execution. Can improve the execution efficiency of the code during the run phase. But JS also provides a way to dynamically change the scope. eval()Functions and withkeywords.

eval()Method:
This method accepts a string as a parameter and treats the content as if it were the code that existed at this position in the program at the time of writing. In other words, you can use the program to generate code and run it in the code you wrote, just as if the code was written in that location.

 function foo(str,a){
     eval(str);//欺骗作用域,词法阶段阶段foo()函数中并没有定义标识符,但是在函数运行阶段却临时定义了一个b; console.log(a,b); } var b = 2; foo("var b =3;",1);//1,3 // 严格模式下,`eval()`会产生自己的作用域,无法修改所在的作用域 function foo(str){ 'use strict'; eval(str); console.log(a);//ReferenceError: a is not de ned } foo('var a =2');

eval()Sometimes it is very useful, but the performance consumption is very large, it may also bring security risks, so it is not recommended.

withKeywords:

with is often used as a shortcut to repeatedly refer to multiple attributes in the same object.

    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; } 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——不好,a被泄漏到全局作用域上了! // 执行了LHS查询,不存在就在全局创建了一个。 // with 声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域。 

withIt will also cause performance loss.

The JavaScript engine performs several performance optimizations during the compilation phase. Some of these optimizations rely on being able to perform static analysis based on the morphology of the code, and pre-determine the definition positions of all variables and functions in order to quickly find the identifier during execution.

Statement promotion

The scope is related to the scope of the identifier, and the scope of the identifier is closely related to its declaration position. In jsthere are some keywords are devoted to (such as the statement identifier var, let, const), will declare non-anonymous defined function identifier.

Regarding the statement, everyone may have heard of the word promotion. Let's analyze the reasons for the promotion of the statement.

We already know that the engine will compile JavaScript code before interpreting it. Part of the job in the compilation phase is to find all the declarations and associate them with the appropriate scope (the core of lexical scope).
In this case, the statement seems to have been mentioned earlier.
It is worth noting that each scope will be promoted. The statement will be promoted to the top of the scope.

However, not all declarations will be promoted, and different declarations will have different weights. Specifically, function declarations will be promoted, and function expressions will not be promoted (even named function expressions will not be promoted).

By var the definition of variables increase, while letand constmade a statement not improve.

Both function declarations and variable declarations will be promoted. But a noteworthy detail is that the function will be promoted first, and then the variable. That is to say, if a variable declaration and a function declaration have the same name, even if the variable declaration is first in the statement order, the identifier will still point to the relevant function.

If a variable or function has repeated declarations, it will be the first declaration.

The last point to note is that the
declaration itself will be promoted, and assignment operations including the assignment of function expressions will not be promoted.

Some applications of scope

Seeing this, I think everyone should have a more detailed understanding of the scope of JS. Let's talk about some extended applications of JS scope.

Principle of least privilege

Also called the principle of minimum authorization or minimum exposure. This principle means that in software design, the necessary content should be exposed to a minimum, and other content should be "hidden", such as the API design of a module or object. That is to privatize part of the code as much as possible.

Functions can generate their own scope, so we can use the method of function encapsulation (both function expressions and function declarations) to achieve this principle.

    // 函数表达式
    var a = 2;
    (function foo() { // <-- 添加这一行 var a = 3; console.log(a); // 3 })(); // <-- 以及这一行 console.log( a ); // 2

Here, by the way, explain how to distinguish between function expressions and function declarations:

If function is the first word in the declaration, then it is a function declaration, otherwise it is a function expression.
The most important difference between function declarations and function expressions is where their name identifiers will be bound. Function expressions can be anonymous, and function declarations cannot omit function names—this is illegal in JavaScript syntax.

It can be encapsulated using the immediately executed function expression (IIFE).

Immediately executed function expression (IIFE)

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

The function expression will be executed immediately after adding a bracket.

(function(){ .. }())It is another expression of IIFE. The brackets are added inside and outside, and the function is the same.

By the way, another very common advanced usage of IIFE is to treat them as function calls and pass parameters in.

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

Generally everyone would describe closures this way.

When the return value of a function is another function, if the returned function calls other variables inside its parent function, if the returned function is executed externally, a closure is generated.

    function foo() {
        var a = 2; function bar() { console.log(a); } return bar; } var baz = foo(); baz(); // 2 —— 这就是闭包的效果。在函数外访问了函数内的标识符 // bar()函数持有对其父作用域的引用,而使得父作用域没有被销毁,这就是闭包

Generally speaking, due to the existence of the garbage collection mechanism, the function will be destroyed after the execution is completed, and the memory space that is no longer used. In the above example, since it looks like  foo()the content will not be used anymore, it is natural to consider recycling it. The "magic" place of closures is that it can prevent this from happening. (Someone used to say that you should reduce the use of closures and fear of memory leaks. In fact, this is not much worried)

In fact, the above definition, I knew it for a long time, but at the same time I also mistakenly thought that I rarely use closures, because I really did not actively use closures, but in fact I was wrong Always use closures.

Essentially, whenever and wherever you treat functions (accessing their respective lexical scopes) as first-level value types and pass them everywhere, you will see the application of closures to these functions. In timers, event listeners, Ajax requests, cross-window communication, Web Workers, or any other asynchronous (or synchronous) tasks, as long as the callback function is used
, you are actually using closures! So you should know that you have Used closures many times.

Here is a pit that everyone may have encountered, a pit caused by a failure to properly understand the scope and closure.

    for (var i = 1; i <= 5; i++) {
        setTimeout(function timer() { console.log(i); }, i * 1000); } // 其实我们想得到的结果是1,2,3,4,5,结果却是五个6

Let's analyze the reasons for this result:
we try to assume that each iteration of the loop will "capture" itself a copy of i at runtime. But according to the working principle of the scope, the actual situation is that although the five functions in the loop are defined in each iteration (the first definition is mainly mentioned before, the latter will be ignored), but they are closed In a shared global scope, because when the time is up to execute the timer function, the i in the global is 6, so it cannot meet expectations.

Understand the problem of scope, here we have two solutions:

    // 办法1
    for (var i = 1; i <= 5; i++) { (function(j) { setTimeout(function timer() { console.log(j); }, j * 1000); })(i); //通过一个立即执行函数,为每次循环创建一个单独的作用域。 } // 办法2 for (var i = 1; i <= 5; i++) { let j = i; // 是的,闭包的块作用域! setTimeout( function timer() { console.log(j); }, j * 1000); } // let 每次循环都会创建一个块作用域

The current development is inseparable from modularity. Let's talk about how modules use closures.

How modules use closures: The
most common way to implement the module pattern is often referred to as module exposure.

Let's see how to define a module

    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

Two necessary conditions of the module:

  • There must be an external closed function, which must be called at least once

  • The closed function must return at least one internal function, so that the internal function can form a closure in the private scope, and can access or modify the private state.

The article is about to end here, thank you for reading, I hope you gain something.

Guess you like

Origin www.cnblogs.com/funtake/p/12733066.html