Intensive reading of JS (5) function closure

Intensive reading of JS (5) function closure

Preface

This article specifically introduces closures , but in fact, the difficulty of closures is not in the concept, but in the nesting of the lexical environment . As long as the nesting relationship of the lexical environment is sorted out clearly, the closure is overcome in an instant. (Or check out Python...)

In short, let's not talk nonsense, the text begins.

Closure

If a function runs outside the defined lexical environment and remembers the lexical environment at the time of its definition, this phenomenon can be called a function closure. For a simple example:

    function f(x=0){
        var count=x;
        
        function getCount(){
            return count++;
        }
        return getCount
    }
    var func = f(10);
    console.log(func());    //10
    console.log(func());    //11
    console.log(func());    //12
    console.log(func());    //13
    console.log(func());    //14
 

First of all, we must understand what happened in the above code, and sort out the process:

  1. fReturns the getCountreference, and will count 10.
  2. In the external lexical environment, it funcpoints togetCount

Then funcit is superimposed magically during execution . For visual representation, I just use a graphical method to represent the environment binding (you can also use the execution context pseudo code):

Note: funcand getCountnot in the same lexical environment.

Then the execution process is:

  1. funcReferenced from getCountthe function pointed to.
  2. Lexical environment that the call funcwill entergetCount
  3. Parse the identifier count, if it is not found in the current lexical environment , enter the external lexical environment ;
  4. Found in the external lexical environment , and superimposed after the return value. --> Save lexical environment state
  5. When it is called again func, it will visit the external lexical environment again , visit count, this time countis 11, and then return.
  6. ...

It can be noticed that the key to forming the closure of the function is that it saves the state of the external lexical environment . But why is it so?

Closure implementation I: The execution context also creates an external environment
  1. When a function is called, an execution context is created for the function .
  2. The environment record of the current lexical environment will be created in the execution context , denoted asCurrentEnvRec
  3. In addition to create CurrentEnvRec, but also create environmental record external lexical environment , denoted byOuterEnvRec
  4. If OuterEnvRecthere is an external lexical environment , then continue to create the environment record of the external lexical environment of OuterEnvRec ...
  5. The above process has been extended to the global environment lexical turned off, the above lexical environment to form a chain, is known as the scope chain (Scope Chain) a.

As mentioned in the environment record , the environment record is used to record the object of the current lexical environment identifier status ; as long as the variable in the environment record exists, it can be accessed .

This scope chain will be stored in the internal properties of the function[[Scope]] , which can be seen in debugging:

Note: ChromeIn, Google browser is specially console.logoptimized, so you can func.prototypesee the value of this attribute.

Closure implementation II: [[Scope]] will not be deleted.

Another special thing about Javascript is that everything is an object. In Javascript, a mark-clearing algorithm is used to release memory; in simple terms, if the object can be accessed, it will always have a mark. When the object can no longer be accessed, the mark will be removed. Released during the traversal cycle.

  • Because the function is also an object, and save the [[Scope]]value of the property.
  • Because the environmental records[[Scope]] have been quoted .
  • Therefore [[Scope]], the environmental records in the system will always be saved.
  • Therefore, the function can always access the value of the external lexical environment and can always be updated .
  • So even if the execution context is destroyed, the lexical environment of the function does not disappear. When the execution context of the function is created again, it just re-points to the lexical environment of the function.

Closure trap

Under normal circumstances, the above troublesome form will not be used, but an anonymous function will be directly returned:

   function f(x=0){
       var count=x;
       
       return function(){
           return count++;
       }
   }
 

Or more streamlined:

 let f=  (x=0)=>{
     var count=x;
     return ()=>count++
 }
 

Occasionally, you will encounter situations that require multiple function closures , namely:

  let f = ()=>{
      let func = []
      for(var i = 0; i < 3; i++){
          func.push(()=>{
              return i*i;
          })
      }
      return func 
  }
 

In this f1 0way, f2 1, , f3 4...... should only right. But the result is:


  let [f1,f2,f3]=f();
  console.log(f1());  //9
  console.log(f2());  //9
  console.log(f3());  //9

 

What is this TMD ?

The reason is very simple, because the variable declared by var is used . The variable declared by var has no block-level scope . The above code is logically equivalent to:

  let f = ()=>{
      let func = []
      var i
      for(i = 0; i < 3; i++){
          func.push(()=>{
              return i*i;
          })
      }
      return func  ///(*)
  }
 

So to sort it out is:

  1. Because func[0]/func[1]/func[2]three closure function of the external environment are lexical lexical environment of the loop body and lexical environment function f :
  2. Query identifier i of the process, the lexical environment of the loop is not found in the identifier i , it uses the function f lexical environment in the identifier i .
  3. Call f1/f2/f3, the loop has ended, so the ifinal value 3. So in the end all return9

We know that the reason that the final result is the same is that f1/f2/f3the identifier i of the function lexical environment is used in the process of parsing the identifier i .

A picture is worth a thousand words:

Therefore: we can create an immediate execution function expression outside the function , and we can directly use the identifier x in the scope of this function .

 let f = ()=>{
     let func = []
     var i
     for(i = 0; i < 3; i++){
         func.push((function(){
             var x=i;
             return ()=>x*x;
         }()))
     }
     return func 
 }
 

Lexical environment nested diagram:

Or use the scope of the loop body directly:

  let f = ()=>{
      let func = []
      var i
      for(i = 0; i < 3; i++){
          let x = i;
          func.push(()=>{
              return x*x;
          })
      }
      return func 
  }
 

Lexical environment nested diagram... (Try it yourself)

In this way, an x will be declared every time and the value of the identifier i will be retained . At this time, the output will be correct:

  let [f1,f2,f3]=f();
  console.log(f1()); //0
  console.log(f2()); //1
  console.log(f3()); //4
 

Knowing the cause and effect, you can simplify the above code. letThe declared count variable will be re-declared each time, and the value after the end of the previous loop is used as the initial count variable . (Refer to the specification CreatePerIterationEnvironment entry for details) namely:

  let f = ()=>{
      let func = []
      for(let i = 0; i < 3; i++){
          func.push(()=>{
              return i*i;
          })
      }
      return func 
  }
 

okay.

At this time, the lexical environment nested graph looks like this:

Supplement: setTimeout

I remember a classic interview question like this:

    for(var i =0;i<6;i++)
       setTimeout(()=>{console.log(i)})
    //  666666
 

I think this, after understanding the above example, this shouldn't be a problem; but be aware that there is a pit in it, so it's like this:

    //
   for(var i =0;i<6;i++)
        setTimeout(console.log, 0, i)
    //0-5
 

It is output normally , because after setTimeoutreceiving it i as a parameter, it will be ipassed to the function inside the function console.log. : which is:

   setTimeout(F, 0, x) 
// setTimeout 
   function setTimeout(F, delay, ...x){
       //.... 
        F.apply(thisObj, x)
        //...
   }    
   window.setTimeout=setTimeout;
 

Note: setTimeoutThe delayfact is the minimum delay time , but the HTML standard requirements, but all begin with less than 4ms 4ms computing .

At last

Next article, start asynchronous. There is one sentence to keep in mind: advancement must be deep, and learning must not always be routine