Detailed explanation of function closures in JavaScript

1. Variable scope

To understand closures, we must first understand the scope of variables. There are two scopes in the ECMAScript5 standard: global scope and function scope .
The calling relationship between the two is:

  • Global variables can be read directly inside the function;
  • Under normal circumstances, the variables declared inside the function cannot be read outside the function;
let num = 1;

function test() {
    
    
  let n = 2;
  console.log(num); // 1
}

test();  
console.log(n); // ReferenceError: n is not defined

In actual development, for various reasons, we must get the local variables inside the function.

The JavaScript language stipulates that all variables of the parent object are visible to the child objects, and vice versa. That is, the "chain scope" structure (chain scope) .
Based on this, we can define another function in the objective function, and this child function can access the internal variables of its parent function normally.

function parent() {
    
    
  let n = 1;
  function child() {
    
    
  console.log(n); // 1
  }
}

Since the child function can get the local variables of the parent function, the parent function directly returns to the child function, and the purpose of accessing the internal variables of the function in the global scope is not achieved.

function parent() {
    
    
  let n = 1;
  function child() {
    
    
  console.log(n);  // 1
  };
  return child;
}

let f1 = parent();
f1();

2. The concept and characteristics of closures

The above example is the simplest way to write a closure: the function child is a closure, so a closure is a "function defined inside a function". In essence, a closure is a bridge connecting the inside and outside of the function.

The closure itself also has the following important characteristics:

  • Nested functions within functions;
  • Inside the closure, you can access the internal parameters, variables or methods of the outer function;
  • The parameters and variables used in the closure will always be stored in the memory, and will not be recycled by the garbage collection mechanism after the function call ends;
  • The same closure mechanism can create multiple closure function instances, which are independent of each other and do not affect each other;

3. The classic way of writing closures

3.1 Function as return value

The above example can be further simplified as an anonymous function:
the internal variable num of the outer function is accessed through the anonymous function, and then the outer function returns the anonymous function, and the anonymous function continues to return the num variable.

function closure1(){
    
    
  let num = 1;
  return function(){
    
    
    return num
  }
}

let fn1 = closure1();
console.log(fn1()); // 1

In this way, a variable fn1 can be declared in the global scope to inherit the num variable, so that the purpose of accessing local variables in the function in the global scope is achieved.

3.1.1 Save variables While the
closure can read the local variables in the function, it can also keep these variables in the memory at all times, and will not be recycled by the garbage collection mechanism after the function call ends.
For example, this example:

function closure2(){
    
    
  let num = 2;
  return function(){
    
    
    let n = 0;
    console.log(n++,num++);
  }
}

let fn2 = closure2();
fn2();  // 0 2
fn2();  // 0 3

Execute the function instance fn2() twice, you can see that the results are slightly different:

  • The output of n++ is the same twice: the
    variable n is the internal variable of the anonymous function. After the anonymous function is called, its memory space will be released normally, that is, it will be recycled by the garbage collection mechanism.

  • The two outputs of num++ are inconsistent: the
    anonymous function refers to the local variable num of its outer function. Even if the call of the anonymous function ends, this dependency still exists, so the variable num cannot be destroyed. The anonymous function that has been stored in the memory will continue to use the result of the last call when the anonymous function is called next time.

Using this feature of closures, it is indeed possible to do simple data caching.
However, closures should not be abused. This can easily increase memory consumption, which can lead to memory leaks or web page performance problems.

3.1.2 Multiple closure functions are independent of each other

The same closure mechanism can create multiple closure function instances, which are independent of each other and do not affect each other.
For example, the following simple example:

function fn(num){
    
    
  return function(){
    
    
    return num++
  }
}

We declare three instances of the closure function separately, passing in different parameters. Then execute 1, 2, 3 times:

function fn(num){
    
    
  return function(){
    
    
    return num++
  }
}

let f1 = fn(10);
let f2 = fn(20);
let f3 = fn(30);

console.log(f1())  // 10
console.log(f2())  // 20
console.log(f2())  // 21
console.log(f3())  // 30
console.log(f3())  // 31
console.log(f3())  // 32

It can be seen that the first execution of f1(), f2(), and f3() sequentially output 10 20 30, and the multiple executions are also accumulated on the results of their last execution, and there is no influence on each other.

3.2 Immediately execute function (IIFE)

In the previous method, the function is only returned as the return value, and the specific function call is written elsewhere. So can we let the outer function directly return the call result of the closure?
The answer is, of course, yes: use the wording of immediate execution function (IIFE) .
Next, let’s first understand what an immediate execution function (IIFE) is:
we all know that the most common way to call a function in JavaScript is to follow the function name with parentheses (). Sometimes, we need to call the function immediately after defining the function. But you cannot add parentheses directly after the function definition, as this will cause a syntax error.

// 提示语法错误
function funcName(){
    
    }();

The reason for the error is that the function keyword can be used as a statement or an expression.

// 语句
function f() {
    
    }

// 表达式
var f = function f() {
    
    }

When used as an expression, the function can be called directly with parentheses after the definition.

var f = function f(){
    
     return 1}();
console.log(f) // 1

In order to avoid the ambiguity of parsing, JavaScript stipulates that if the function keyword appears at the beginning of the line, it shall be interpreted as a sentence. So if we still want to use the function keyword to declare a function to be able to call it immediately, we need to make the function not appear directly at the beginning of the line, and let the engine understand it as an expression.
The easiest way to deal with it is to put it inside a parenthesis.

(function(){
    
     /* code */ }());
// 或者
(function(){
    
     /* code */ })();

This is called "Immediately-Invoked Function Expression" (Immediately-Invoked Function Expression), that is, immediate execution function, referred to as IIFE.

3.2.1 The classic loop output problem of timer setTimeout
After understanding the immediate execution of the function, let's quickly look at an example: use the for loop to output 1~5 in turn. So if it is the following code, what is the result of its operation?

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

The result must be 5 6s. The reason is that the for loop belongs to the synchronous task, and the setTimeout timer belongs to the macro task category of the asynchronous task. The JavaScript engine will execute the synchronized main thread code first, and then execute the macro task.

So before executing the setTimeout timer, the for loop has ended, and the loop variable i = 6 at this time. Then the setTimeout timer is created 5 times in a loop, and 5 6s are output after all executions are completed.

But our purpose is to output 1~5, which obviously does not meet the requirements. Before formally introducing the writing method of the immediate execution function (IIFE), I will talk about another method: the loop variable i is declared with the let keyword.

for (let i = 1; i <= 5; i++) {
    
    
  setTimeout( function timer() {
    
    
    console.log(i); // 1 2 3 4 5
  }, 1000 );
}

Why can it be replaced by let statement? It is because the essential requirement to realize 1~5 loop output is to remember the value of the loop variable during each loop .
And let's statement is just enough. Portal: Detailed explanation of var, let, const features and differences in JavaScript

Let’s look at the writing of the immediate execution function (IIFE):

for (var i = 1; i <= 5; i++) {
    
    
  (function(i){
    
    
    setTimeout( function timer() {
    
    
      console.log(i); // 1 2 3 4 5
    }, 1000 );
  })(i);
}

The setTimeout timer function is wrapped in an outer anonymous function to form a closure, and then the immediate execution function (IIFE) is used: continue to wrap the outer anonymous function in parentheses, and then follow the parentheses to call, and put every The second loop variable is passed in as a parameter.
In this way, the result of each loop is the call result of the closure: output the value of i; according to one of the characteristics of the closure itself: variables or parameters can be saved, all the conditions are met and 1~5 are output correctly.

To say more, the current output format is to output 1~5 at the same time after one second; then I want to output one of these five numbers every second?

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

The second parameter of each setTimeout timer can be controlled: the interval duration, which is multiplied by the loop variable i in turn.
The effect is as follows:
Insert picture description here
3.2.2
Passing in functions as formal parameters of API The mechanism of closure combined with immediate execution function (IIFE) has another very important use: various APIs that require functions as formal parameters.
Take the array's sort() method as an example: The Array.prototype.sort() method supports passing in a comparator function to allow us to customize the sorting rules. The comparator function must have a return value, it is recommended to return Number type.

For example, the following array scenario: We hope you can write a mySort() method: you can sort the array elements in descending order according to any specified attribute value.
The mySort() method definitely requires two formal parameters: the array arr to be sorted and the specified property value property.
In addition, the API used must still be the sort() method. Here we can’t directly pass in a comparator function, but use the closed IIFE method of writing: the
property value property is passed as a parameter to the outer anonymous function, and then the anonymous function returns internally. The final comparator function required by the sort() method.

var arr = [
  {
    
    name:"code",age:19,grade:92},
  {
    
    name:"zevi",age:12,grade:94},
  {
    
    name:"jego",age:15,grade:95},
];

function mySort(arr,property){
    
    
  arr.sort((function(prop){
    
    
    return function(a,b){
    
    
       return a[prop] > b[prop] ? -1 : a[prop] < b[prop] ? 1 : 0;
    }
  })(property));
};


mySort(arr,"grade");
console.log(arr); 
/*
[
  {name:"jego",age:15,grade:95},
  {name:"zevi",age:12,grade:94},
  {name:"code",age:19,grade:92},
]
*/

3.3 Private properties and private methods of encapsulated objects

Closures can also be used to encapsulate objects, especially to encapsulate private properties and private methods of
objects : we encapsulate an object Person, which has a public property name, a private property _age, and two private methods.
We cannot directly access and modify the private attribute _age, we must call its internal closures getAge and setAge.

function Person(name) {
    
    
  var _age;
  function setAge(n) {
    
    
    _age = n;
  }
  function getAge() {
    
    
    return _age;
  }

  return {
    
    
    name: name,
    getAge: getAge,
    setAge: setAge
  };
}


var p1 = Person('zevin');
p1.setAge(22);
console.log(p1.getAge()); // 22

4. Advantages and disadvantages of using closures

4.1 Advantages

4.1.1 Realize encapsulation and protect the safety of variables in functions. The
use of closures can save variables in memory and will not be destroyed by the system's garbage collection mechanism, thus playing a role in protecting variables.

function closure2(){
    
    
  let num = 1;
  return function(){
    
    
    console.log(num++)
  }
}

let fn2 = closure2();
fn2();  // 1
fn2();  // 2

4.1.2 Avoid the pollution of global variables The
use of global variables should be avoided as much as possible in development to prevent unnecessary naming conflicts and call confusion

// 报错
var num = 1;
function test(num){
    
    
  console.log(num)
}

test();
let num = test(4);
console.log(num);

At this time, you can choose to declare the variable inside the function and use the closure mechanism.
This not only ensures the normal call of variables, but also avoids the pollution of global variables.

function test(){
    
    
  let num = 1;
  return function(){
    
    
    return num
  }
}

let fn = test();
console.log(fn());

4.2 Disadvantages

4.2.1 Memory consumption and memory leakage
Each time the outer function runs, a new closure will be generated, and this closure will retain the internal variables of the outer function, so the memory consumption is high.
Solution: Do not abuse closures.
At the same time, the internal variables referenced in the closure will be saved and cannot be released, which also causes the problem of memory leaks.
Solution:

window.onload = function(){
    
    
  var userInfor = document.getElementById('user');
  var id = userInfor.id;
  oDiv.onclick = function(){
    
    
    alert(id);
  }
  userInfor = null;
}

Before using the variable userInfor in the internal closure, use another variable id to inherit it, and manually assign the variable userInfor to null after using it.

Guess you like

Origin blog.csdn.net/ZYS10000/article/details/113834843