An article explaining JS closures in detail!

Closure in JavaScript is a very important concept and is closely related to the direction of scope-related knowledge. It is often mentioned during the front-end interview process of major manufacturers.

Basic introduction to scope

In layman’s terms, the scope of JavaScript refers to the scope within which a variable can be accessed. There are several types of scope in JavaScript. Before ES5, there were only two types: global scope and function scope. After the emergence of ES6, block-level scope was added. Let’s take a look at the concepts of these three scopes to lay a solid foundation for learning closures.

global scope

In programming languages, whether Java or JavaScript, variables are generally divided into two types: global variables and local variables. Then the variables are defined outside the function, and the first ones in the code are usually global variables.

In JavaScript, global variables are variables mounted under the window object, so you can use and access this global variable anywhere in the web page. Let’s look at a piece of code to explain what the global scope is.

var globalName = 'global' ;

function  getName () { 
   console . log (globalName) // global 
  var name = 'inner' 
  console . log (name) // inner
}

getName ();
 console . log (name); // 
console . log (globalName); //global 
function  setName (){
  vName = 'setName' ;
}

setName ();
 console . log (vName); // setName 
console . log ( window . vName ) // setName

From this code we can see that the globalName variable can be accessed no matter where it is, so it is a global variable. The name variable used as a local variable in the getName function does not have this capability.

If all variables that are not defined in JavaScript and are directly assigned a value are by default a global variable, such as the vName variable in the setName function in the above code.

We can find that global variables also have global scope. You can use it no matter where you are. When you enter window.vName in the browser console, you can access all global variables on the window.

Of course, global scope has corresponding shortcomings. When we define many global variables, it will easily cause variable naming conflicts, so we should pay attention to scope issues when defining variables.

function scope

In JavaScript, variables defined in a function are called function variables. At this time, they can only be accessed inside the function, so its scope is also inside the function, which is called function scope. Let’s look at a piece of code.

function  getName () {
   var name = 'inner' ;
   console . log (name); //inner
}

getName ();
 console . log (name);

In the above code, the variable name is defined in the getName function, so name is a local variable, and its scope is inside the getName function, also called the function scope.

It cannot be accessed from other places except inside this function. At the same time, when this function is executed, the local variable will be destroyed accordingly. So you will see that the name outside the getName function is not accessible.

Let’s look at the last block-level scope.

block scope

There is a new block-level scope in ES6. The most direct manifestation is the new let keyword. Variables defined using the let keyword can only be accessed in the block-level scope, which has the characteristics of “temporary dead zone”. In other words, this variable cannot be used before it is defined.

It sounds like you still don’t quite understand the meaning of block-level scope, so let’s give a more vivid example to see what exactly is block-level scope? In fact, in the JS coding process, what is included in the {…} after the if statement and the for statement is the block-level scope.

Let’s illustrate it with a piece of code.

console . log (a) //a is not defined

if ( true ){
   let a = '123' ;
   console . log (a); // 123
}

console . log (a) //a is not defined

It can be seen from this code that variable a is a variable defined by the let keyword in the if statement {…}, so its scope is the part in the brackets of the if statement, and the a variable is accessed outside An error will be reported because this is not its scope. Therefore, if the result of the variable a is output before and after the if code block, the console will show that a is not defined.

So after laying the foundation for the above concepts of scope, we can learn the concept of closure.

What is a closure?

Let’s first look at the concept of closures given in the Red Book and MDN.

The definition of closure in the Red Book: A closure refers to a function that has access to variables in the scope of another function.MDN: A function is bundled with a reference to its surrounding state (or the function is surrounded by references). This combination is a closure. In other words, closures allow you to access the scope of an outer function from within an inner function.

At first glance, the two more official definitions above are difficult to understand clearly, especially MDN’s definition of closure, which is really “dizzy”. To explain it in layman’s terms: a closure is actually an accessible Functions with variables inside other functions. That is, a function defined inside a function, or it can be said that the closure is an embedded function.

Because usually, the internal variables of a function cannot be accessed externally (that is, the difference between global variables and local variables), so using closures can realize the function of accessing the internal variables of a function externally, allowing these The values ​​of internal variables can always be saved in memory. Let’s first look at a simple example through code.

function  fun1 () {
     var a = 1 ;
     return  function (){
         console . log (a);
    };
}

fun1 ();
 var result = fun1 ();
 result ();   // 1

Combined with the concept of closure, if we put this code into the console and execute it, we can find that the final output result is 1 (that is, the value of the a variable). Then it can be clearly found that the a variable, as an internal variable of the fun1 function, is normally a local variable within the function and cannot be accessed from the outside. But through closure, we can finally get the value of variable a.

The reason for closure

We introduced the concept of scope earlier, so you also need to understand the basic concept of scope chain. In fact, it is very simple. When accessing a variable, the code interpreter will first search in the current scope. If it is not found, it will go to the parent scope to search until the variable is found or does not exist in the parent scope. This way A link is a scope chain.

It should be noted that each sub-function will copy the scope of its superior, forming a chain of scopes. So let’s use the following code to explain the scope chain in detail.

var a = 1 ;
 function  fun1 () {
   var a = 2 
  function  fun2 () {
     var a = 3 ;
     console . log (a); //3
  }

}

It can be seen from this that the scope of the fun1 function points to the global scope (window) and itself; the scope of the fun2 function points to the global scope (window), fun1 and itself; and the scope is looked up from the bottom, Until the global scope window is found, an error will be reported if the global scope does not exist yet.

So this vividly explains what a scope chain is, that is, the current function generally has a reference to the scope of the upper function, then they form a scope chain.

It can be seen that the essence of closure is that there is a reference to the parent scope in the current environment. So let’s take the above code as an example.

function  fun1 () {
   var a = 2 
  function  fun2 () {
     console . log (a);   //2
  }
  return fun2;
}

var result = fun1 ();
 result ();

As can be seen from the above code, the result here will get the variables in the parent scope and output 2. Because in the current environment, there is a reference to the fun2 function, and the fun2 function exactly refers to the scope of window, fun1 and fun2. Therefore the fun2 function is a variable that can access the scope of the fun1 function.

Does that mean that only the return function is considered a closure? Actually not, back to the essence of closure, we only need to let the reference of the parent scope exist, so we can also change the code like this, as shown below.

var fun3;

function  fun1 () {
   var a = 2 
  fun3 = function () {
     console . log (a);
  }
}
fun1 ();
 fun3 ();

It can be seen that the result of the implementation is actually the same as the effect of the previous code, that is, after assigning a value to the fun3 function, the fun3 function has access rights to the scopes of window, fun1 and fun3 itself; then it still starts from Search from bottom to top until you find that the variable a exists in the scope of fun1; therefore, the output result is still 2, and finally a closure is generated. The form has changed, but the essence has not changed.

Therefore, whether the final return is a function or not, it does not mean that no closure is generated. At this point you can have a deeper understanding of the connotation of closure.

The expression of closure

So after understanding the essence of closure, let’s take a look at the manifestations and application scenarios of closure. I have summarized about four scenarios.

  1. Returns a function. The reason has been mentioned above, so I won’t go into details here.
  2. Whenever you use callback functions in timers, event listeners, Ajax requests, Web Workers, or anything asynchronous, you are actually using closures. Please look at the following code, these are the forms used in daily development.// timer setTimeout ( function handler (){ console . log ( ‘1’ ); }, 1000 ); //Event listening $( ‘#app’ ). click ( function (){ console . log ( ‘Event Listener’ ); });
  3. The form passed as a function parameter, such as the following example.var a = 1 ; function foo (){ var a = 2 ; function baz (){ console . log (a); } bar (baz); } function bar ( fn ){ // This is the closure fn (); } foo (); // output 2 instead of 1
  4. IIFE (immediate function execution) creates a closure and saves the global scope (window) and the scope of the current function, so global variables can be output, as shown below.var a = 2 ; ( function IIFE (){ console . log (a); // Output 2 })(); IIFE This function is a little special. It is a self-executing anonymous function. This anonymous function has an independent scope. This not only prevents external access to the variables in this IIFE, but also does not pollute the global scope. We often see such functions in advanced JavaScript programming.

You already have a certain understanding of the basic concepts, causes and manifestations of closures. So in the last part, let’s look at a more common development application scenario.

How to solve the loop output problem?

In interviews with major Internet companies, solving loop output problems is a relatively frequent interview question. You will usually be given a piece of code like this to explain. So based on the content of this lesson, let’s take a look at this question here. code show as below.

for ( var i = 1 ; i <= 5 ; i ++){
   setTimeout ( function () {
     console . log (i)
  }, 0 )
}

After the above code is executed, it can be seen from the console execution result that the output is 5 6s. Then the interviewer will usually ask why they are all 6s first? What if I want you to output 1, 2, 3, 4, 5?

Therefore, based on the knowledge we have learned in this lecture, let’s think about how to give the interviewer a satisfactory explanation. You can answer around these two points.

  1. setTimeout is a macro task. Due to the single-threaded eventLoop mechanism in JS, the macro task is executed only after the main thread synchronization task is executed. Therefore, the callbacks in setTimeout are executed sequentially after the loop ends.
  2. Because the setTimeout function is also a closure, its parent scope chain is window. The variable i is a global variable on the window. Before the setTimeout is executed, the variable i is already 6, so the final output sequence is 6.

So let’s take a look at how to output 1, 2, 3, 4, and 5 in order?

Utilize IIFE

You can use IIFE (immediate execution function). Each time the for loop is executed, the variable i at this time is passed to the timer and then executed. The modified code is as follows.

for ( var i = 1 ;i <= 5 ;i++){
  ( function ( j ) {
     setTimeout ( function  timer (){
       console . log (j)
    }, 0 )
  })(i)
}

It can be seen that by using IIFE (immediate execution function) in this way, the sequential output of serial numbers can be achieved.

Using let in ES6

The new let method of defining variables in ES6 has revolutionized JS after ES6, giving JS a block-level scope, and the scope of the code is executed in block-level units. Through the modified code, the desired results above can be achieved.

for ( let i = 1 ; i <= 5 ; i++){
   setTimeout ( function () {
     console . log (i);
  }, 0 )
}

As can be seen from the above code, by redefining the i variable by using let to define the variable, this problem can be solved with the least change cost.

The timer passes in the third parameter

As a frequently used timer, setTimeout has a third parameter. In daily work, we usually use the first two, one is the callback function, the other is the time, and the third parameter is rarely used. Then combined with the third parameter, the code after adjustment is as follows.

for ( var i= 1 ;i<= 5 ;i++){
   setTimeout ( function ( j ) {
     console . log (j)
  }, 0 , i)
}

It can be seen from this that the passing of the third parameter can change the execution logic of setTimeout to achieve the results we want. This is also a way to solve the problem of loop output.