[js] scope, scope chain and closure

1. Scope

The scope is an independent area . To be more specific, it is an independent area where variables are defined in our program , which determines the access rights of the currently executing code to variables .

There are two kinds of scopes in JavaScript:

  • global scope
  • local scope

If a variable is declared outside the function, or outside the code block, that is, outside the curly braces , then a global scope{} is defined. Before ES6, the local scope only included the function scope. The block-level scope provided by ES6 , also belongs to the local scope .

function fun() { 
    //局部(函数)作用域
    var innerVariable = "inner"
} 
console.log(innerVariable) 
// Uncaught ReferenceError: innerVariable is not defined

In the above example, the variable is declared in innerVariablethe function, that is, in the local scope , but not in the global scope, so the output in the global scope will report an error.

In other words, a scope is an independent area where variables are not exposed to the outside world. The biggest use of scope is to isolate variables , and variables with the same name in different scopes will not conflict.

function fun1(){
    var variable = 'abc'
}
function fun2(){
    var variable = 'cba'
}

In the above example, there are two functions, both of which have a variable with the same name variable, but they are located in different functions, that is, in different scopes, so they will not conflict.

Before ES6 JavaScript did not have block-level scope, only global scope and function (local) scope. Block statements ( {}statements in between), such as  if and  switch conditional statements or  for and  while loop statements, do not create a new scope , unlike functions .

if(true) {
    var a = 1
}
for(var i = 0; i < 10; i++) {
    ...
}
console.log(a) // 1
console.log(i) // 9

2. Global scope

Objects that can be accessed anywhere in the code have a global scope. Generally speaking, the following situations have a global scope:

1. The outermost function and variables defined outside the outermost function have global scope

var outVariable = "我是最外层变量";//最外层变量 
function outFun() {               //最外层函数 
    var inVariable = "内层变量" 
    function innerFun() {         //内层函数 
        console.log(inVariable) 
    } 
    innerFun()
} 
console.log(outVariable) //我是最外层变量 
outFun()                 //内层变量 
console.log(inVariable)  //inVariable is not defined 
innerFun()               //innerFun is not defined

In the above example, after the call , the function outFun()inside  is executed and the variable is output. Because the inner scope can access the outer scope , it can be output normally . But the next line of output  is in the global scope and cannot access variables in the local scope, so the variable will not be accessible.innerFuninVariable内层变量inVariable 

2. All variables that do not define direct assignment (also known as unexpected global variables) are automatically declared to have global scope

function outFun2() {
    variable = "未定义直接赋值的变量";
    var inVariable2 = "内层变量2";
}
outFun2();                //要先执行这个函数,否则根本不知道里面有什么
console.log(variable);    //“未定义直接赋值的变量”
console.log(inVariable2); //inVariable2 is not defined

3. All properties of the window object have global scope

In general, the built-in properties of the window object have global scope, such as window.name, window.location, window.document, window.history, and so on.

The global scope has a disadvantage: if we write many lines of JS code, the variable definition is not included in the function, then they are all in the global scope. This will pollute the global namespace and easily cause naming conflicts .

// A写的代码中
var data = {a: 1}

// B写的代码中
var data = {b: 2}

That's why the source code of jQuery, Zepto and other libraries, all the code will be placed in (function(){....})()( immediate execution function ). Because all variables placed inside will not be leaked and exposed, will not pollute the outside, and will not affect other libraries or JS scripts. This is a manifestation of function scope.

3. Local scope

In contrast to the global scope, the local scope is generally only accessible within a fixed piece of code . Local scope is divided into function scope and block scope .

3.1 Function scope

Function scope refers to variables or functions declared inside a function .

function doSomething(){
    var name = "Rockky";
    function innerSay(){
        console.log(name);
    }
    innerSay();
}
console.log(name); //name is not defined
innerSay(); //innerSay is not defined

Scopes are hierarchical. Inner scopes can access variables in outer scopes, but not vice versa . Let's look at an example. It may be easier to understand the scope by using bubbles as a metaphor: 

 The final output result is 2, 4, 12

  • Bubble 1 is the global scope and has the identifier foo;
  • Bubble 2 is scope foo with identifiers a, bar, b;
  • Bubble 3 is scope bar and has only identifier c.

3.2 Block scope

ES5 only has global scope and function scope, but no block-level scope, which brings many unreasonable scenarios.

In the first scenario, inner variables may override outer variables.

var tmp = new Date();
function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}
f(); // undefined

The original meaning of the above code is that ifthe outside of the code block uses tmpthe variables of the outer layer, and the inside uses tmpthe variables of the inner layer. However, fafter the function is executed, the output result is undefined, the reason is that the variable is promoted , causing the inner tmpvariable to cover the outer tmpvariable, and tempthe initialization of the variable will not be promoted, that is, the variable is declared but not initialized , so the value of temp is undefined.

Note : The scope of variable promotion is the entire function , and the function declared by var will be promoted to the top of the scope. This means that all places in the function are the scope of variable promotion, but only to the top of the scope .

In the second scenario, the loop variable used for counting is leaked as a global variable.

var s = 'hello';
for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}
console.log(i); // 5

In the above code, the variable iis only used to control the loop, but after the loop ends, it does not disappear and leaks into a global variable .

ES6's block-level scope solves these problems to a certain extent.

Block-level scope can be declared through the new commands let and const, and the declared variables cannot be accessed outside the scope of the specified block. Block scopes are created when:

  1. inside a function
  2. Inside a code block (wrapped by a pair of curly braces)

The syntax of a let declaration is identical to that of var. Basically, let can be used instead of var for variable declaration, but the scope of the variable will be limited to the current code block. Block-level scope has the following characteristics:

  • Declaring a variable does not hoist to the top of the code block, i.e. there is no variable hoisting
  • Do not declare the same variable repeatedly
  • The Magic of Bound Block Scoping in Loops

3.2.1 Variable promotion

varCommands undergo "variable hoisting" , i.e. variables can be used before they are declared with a value of undefined. This phenomenon is more or less strange. According to the general logic, variables should be used after the declaration statement. In order to correct this phenomenon, letthe command changes the grammatical behavior, and the variables it declares must be used after the declaration, otherwise an error will be reported.

// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

In the above code, the variable is declared foowith varthe command, and variable promotion will occur, that is, when the script starts to run, the variable fooalready exists, but has no value, because the JS engine will only promote the declaration of the variable, and will not promote the initialization of the variable . is equivalent to the following code: 

// var 的情况
var foo;
console.log(foo); // 输出undefined
foo = 2;

So will output undefined. Variables are declared barwith letcommands, no variable hoisting occurs. barThis means that the variable does not exist until it is declared , and an error will be thrown if it is used.

If there are functions and variables declared at the same time , which one will perform variable promotion?

console.log(foo); 
var foo = 'abc'; 
function foo(){}

The output is function foo(){}, that is, the function content. What if it is another form?

console.log(foo);
var foo = 'abc';
var foo = function(){}

The output is undefined an analysis of two results:

  • The first type: function declaration . It is the first one above, function foo(){}this form
  • The second type: function expression . It is the second one above, var foo = function(){}this form

The second form is actually the declaration and definition of the var variable, so it should be understandable that the second output above is undefined. The first form of function declaration, when promoted, will be promoted as a whole, including the part of function definition! So the first form is equivalent to the following way!

var foo = function(){}
console.log(foo);
var foo ='abc';
  • function declarations are hoisted to the top;
  • The declaration is made only once, so subsequent var foo='abc'declarations are ignored.
  • The priority of the function declaration is higher than that of the variable declaration , and the function declaration will be promoted together with the definition (here is different from the variable)

As long as there is a command in the block-level scope let, the variables it declares "bind" (binding) this area and are no longer affected by the outside world.

var tmp = 123;
if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

In the above code, there is a global variable , but a local variable is declared tmpin the block-level scope , causing the latter to bind the block-level scope, so before declaring the variable, an error will be reported for the assignment.lettmplettmp

ES6 clearly stipulates that if there are letand constcommands in the block, the variables declared by the block for these commands form a closed scope from the beginning. Any use of these variables before declaration will result in an error.

In short, within a code block, leta variable is not available until it is declared with a command . This is grammatically called "temporal dead zone" (temporal dead zone, TDZ for short).

if (true) {
  // 暂时性死区开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // 暂时性死区结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

In the above code, before letthe command declares the variable tmp, it belongs to the tmp"dead zone" of the variable.

A "temporary dead zone" also means that typeofit is no longer a 100 percent safe operation.

typeof x; // ReferenceError
let x;

In the above code, the variable is declared xusing letthe command, so before the declaration, it belongs to xthe "dead zone", as long as the variable is used, an error will be reported. Therefore, typeofone is thrown at runtime ReferenceError.

As a comparison, if a variable is not declared at all, it typeofwill not report an error when used.

typeof undeclared_variable // "undefined"

In the above code, undeclared_variableit is a variable name that does not exist, and the result returns "undefined". Therefore, letbefore there is no operator, typeofit is 100% safe and will never report an error. This is no longer true. This design is to let everyone develop good programming habits. Variables must be used after declaration, otherwise an error will be reported.

Some "dead zones" are hidden and not easy to find.

function bar(x = y, y = 2) {
  return [x, y];
}
bar(); // 报错

In the above code, barthe reason why the calling function reports an error (some implementations may not report an error) is because the xdefault value of the parameter is equal to another parameter y, and yit has not been declared at this time, which belongs to the "dead zone". If ythe default value is x, no error will be reported, because it xhas been declared at this time.

function bar(x = 2, y = x) {
  return [x, y];
}
bar(); // [2, 2]

In addition, the following code will also report an error, which varbehaves differently from .

// 不报错
var x = x;
// 报错
let x = x;  // ReferenceError: x is not defined

The above code reports an error because of the temporary dead zone. When using letthe declared variable, as long as the variable is used before the declaration is completed, an error will be reported. The above line belongs to this situation. Before the declaration statement of the variable xis executed, xthe value is fetched, resulting in an error "x is undefined" .

ES6 stipulates that the temporary dead zone and letthe conststatement do not have variable promotion , mainly to reduce runtime errors and prevent the variable from being used before the variable is declared, resulting in unexpected behavior. Mistakes like this are common in ES5, and now with this provision, it's easy to avoid them.

In short, the essence of the temporary dead zone is that as soon as it enters the current scope, the variable to be used already exists, but it cannot be obtained. Only when the line of code declaring the variable appears, can the variable be obtained and used.

let/const Declarations are not hoisted to the top of the current block, so you need to manually place  let/const the declaration at the top to make the variable available throughout the block.

function getValue(condition) {
    if (condition) {
        let value = "blue";
        return value;
    } else {
        // value 在此处不可用
        return null;
    }
    // value 在此处不可用
}

The advent of block-level scope actually makes the widely used anonymous immediately executed function expressions (anonymous IIFE) unnecessary.

// IIFE 写法
(function () {
  var tmp = ...;
  ...
}());

// 块级作用域写法
{
  let tmp = ...;
  ...
}

3.2.2 Duplicate declaration

If an identifier is already defined inside a code block, then using the same identifier in a let declaration within this code block will cause an error to be thrown. For example:

// 报错
function func() {
  let a = 10;
  var a = 1;
}

// 报错
function func() {
  let a = 10;
  let a = 1;
}

let But if you declare a new variable with the same name in a nested scope  , no error will be thrown.

var count = 30;
if (condition) {
    let count = 40; // 不会抛出错误
}

Also, parameters cannot be redeclared inside a function.

function func(arg) {
  let arg;
}
func() // 报错

function func(arg) {
  {
    let arg;
  }
}
func() // 不报错

In addition, there is another place to pay attention to. ES6's block-level scope must have curly braces. If there are no curly braces, the JavaScript engine will think that there is no block-level scope.

// 第一种写法,报错
if (true) let x = 1;

// 第二种写法,不报错
if (true) {
  let x = 1;
}

3.2.3 for loop

Developers may most want to achieve forblock-level scope of the loop, because the declared counter variable can be limited to the loop, for example:

for (let i = 0; i < 10; i++) {
  // ...
}
console.log(i); // ReferenceError: i is not defined

In the above code, the counter is ionly forvalid within the loop body, and an error will be reported if it is referenced outside the loop.

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

In the above code, the variable iis declared by the var command and is valid in the global scope, so there is only one variable globally i. Every loop, ithe value of the variable will change, and inside athe function that is assigned to the array in the loop console.log(i), the i inside points to the global i. That is to say, all athe members of the array ipoint to the same one i, which causes the value of the last round to be output at runtime i, which is 10.

If used let, the declared variable is only valid in the block-level scope, and the final output is 6.

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

In the above code, the variable iis letdeclared, and the current one iis valid in the current cycle, so each cycle iis actually a new variable, so the final output is 6. You may ask, if the variables of each cycle iare redeclared, how does it know the value of the previous cycle, so as to calculate the value of the current cycle? This is because the JavaScript engine internally remembers the value of the previous cycle, and iwhen initializing the variables of the current cycle, it calculates on the basis of the previous cycle.

Consider the following example:

<button>点我打印</button>
<button>点我打印</button>
<button>点我打印</button>

let el = document.querySelectorAll('button')
let i
for (i = 0; i < 3; i++) {
  el[i].addEventListener('click', function (event) {
     console.log(i)
  })
}

Click the button button in turn, what will be printed out?

the answer is:3,3,3

Because let i it is declared under the global, the change of each loop in the for loop is global , similar to the example iabove with the declaration, so the final output is 3.var

In addition, forthere is another special feature of the loop, that is, the part where the loop variable is set is a parent scope, and the inside of the loop body is a separate child scope.

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

The above code runs correctly and outputs 3 times abc. This indicates that the variables inside the function are not in the same scope ias the loop variables i, and have their own separate scopes (the same scope cannot be used to  let declare the same variable repeatedly).

4. Scope chain

In the following code, console.log(a)the variable a is to be obtained, but a is not defined in the current scope (compared with b). Variables that are not defined in the current scope, will be  自由变量 . How to get the value of the free variable? It will look outward layer by layer from the parent scope until it finds the global windowobject, that is, the global scope. If it is not in the global scope, it will returnundefined . Similar to looking up variables from the inside to the outside along a chain , this chain is called the scope chain . The inner environment can access all outer environments through the scope chain, but the outer environment cannot access any variables and functions of the inner environment.

var a = 100
function fn() {
    var b = 200
    console.log(a) // 这里的a在这里就是一个自由变量
    console.log(b)
}
fn() // 100

Let's look at another example

var x = 10
function fn() {
   console.log(x)
}
function show(f) {
   var x = 20
   (function() {
      f()   //10,而不是20
   })()
}
show(fn)

In the fn function, when fetching the value of the free variable x, in which scope should it be fetched? ——To be retrieved from the scope where the fn function was created, no matter where the fn function will be called .

Therefore, it may be more appropriate to use this sentence to describe the value-taking process of free variables: to get the value in the scope that created this function, the emphasis here is on "creation" rather than "calling" (somewhat similar to arrow function This points to)  , remember to remember - in fact, this is the so-called "static scope"

var a = 10
function fn() {
  var b = 20
  function bar() {
    console.log(a + b) //30
  }
  return bar
}
var x = fn(),
b = 200
x() //bar()

fn()What is returned is bara function, assigned to x. Execute x(), that is, execute barthe function code. When the value is taken b, fnit is taken out directly in the scope. When taking athe value, I tried to get it in fnthe scope, but I couldn’t get it, so I had to turn to fnthe created scope to find it, and I found it, so the final result is30

5. Closure

(1) Basic concept of closure

The definition of closure in MDN:

A function is bundled with references to its surrounding state (or the function is surrounded by references), and such a combination is a closure. That is, closures allow you to access the scope of an outer function from within an inner function.

In layman's terms, a closure is actually a function that can access variables inside other functions. That is, a function defined inside a function, or a closure is an embedded function.

Usually, the internal variables of a function cannot be accessed externally (that is, the difference between global variables and local variables), so the use of closures has the function of being able to access internal variables of a function externally, allowing these internal The value of a variable can always be kept in memory:

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

The output of this code in the console is 1 (the value of a). It can be found that the a variable is an internal variable of the fun1 function. Normally, as a local variable in the function, it cannot be accessed by the outside. But through the closure, you can finally get the value of the a variable.

Intuitively, the concept of closure provides a way and convenience for accessing variables in functions in JavaScript . There are many advantages to doing this, for example, you can use closures to implement caching, etc.

(2) Causes of closure

The concept of scope was mentioned earlier, we also need to know the basic concept of scope chain. When accessing a variable, the code interpreter will first search in the current scope, if not found, go to the parent scope to search until the variable is found or does not exist in the parent scope, such a link is the function domain chain.

It should be noted that each sub-function will copy the scope of the parent to form a scope chain:

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

It can be seen 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 searched from the bottom up until Until the global scope window is found, if there is no global scope, an error will be reported. This vividly illustrates 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 generation is that there is a reference to the parent scope in the current environment .

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

As you can see, 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 just refers to the window, fun1 and fun2 scopes. So the fun2 function is a variable that has access to the scope of the fun1 function.

Is it only the return function that generates a closure? In fact, it is not. Back to the essence of the closure, it is only necessary to let the reference of the parent scope exist , so the above code can be modified like this:

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

It can be seen that the implemented result is actually the same as the effect of the previous piece of code, that is, after assigning a value to the fun3 function, the fun3 function has the access rights of window, fun1 and fun3 itself; and then from Search down and up until you find the variable a 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.

(3) Closure application scenarios

Let's take a look at the manifestation and application scenarios of closures:

  • In timers, event listeners, Ajax requests, Web Workers or anything asynchronous , as long as you use a callback function, you are actually using a closure:
// 定时器
setTimeout(function handler(){
  console.log('1');
},1000);

// 事件监听
document.getElementById(app).addEventListener('click', () => {
  console.log('Event Listener');
});
  • The form passed as a function parameter:
var a = 1;
function foo(){
  var a = 2;
  function baz(){
    console.log(a);
  }
  bar(baz);
}
function bar(fn){
  // 这是闭包
  fn();
}
foo();  // 输出2,而不是1
  • IIFE (immediate execution function) , creates a closure, saves the global scope (window) and the scope of the current function, so global variables can be output:
var a = 2;
(function IIFE(){
  console.log(a);  // 输出2
})();

IIFE is a self-executing anonymous function that has an independent scope. This not only prevents the outside world from accessing the variables in this IIFE, but also does not pollute the global scope.

  • Result cache (memo mode)

The memo pattern is a typical application of the characteristics of application closures. For example the following function:

function add(a) {
    return a + 1;
}

When add() is executed multiple times, the result obtained each time is recalculated. If it is a computational operation with a large cost, it will consume performance. Here, a cache can be made for the input that has been calculated. So here you can use the characteristics of closures to implement a simple cache, and use an object to store the input parameters inside the function. If you enter the same parameters next time, then compare the properties of the objects. If there is a cache, just put it directly Values ​​are fetched from this object. The implementation code is as follows:

function memorize(fn) {
    var cache = {}
    return function() {
        var args = Array.prototype.slice.call(arguments)
        var key = JSON.stringify(args)
        return cache[key] || (cache[key] = fn.apply(fn, args))
    }
}

function add(a) {
    return a + 1
}

var adder = memorize(add)

adder(1)            // 输出: 2    当前: cache: { '[1]': 2 }
adder(1)            // 输出: 2    当前: cache: { '[1]': 2 }
adder(2)            // 输出: 3    当前: cache: { '[1]': 2, '[2]': 3 }

Implemented using ES6:

function memorize(fn) {
    const cache = {}
    return function(...args) {
        const key = JSON.stringify(args)
        return cache[key] || (cache[key] = fn.apply(fn, args))
    }
}

function add(a) {
    return a + 1
}

const adder = memorize(add)

adder(1)            // 输出: 2    当前: cache: { '[1]': 2 }
adder(1)            // 输出: 2    当前: cache: { '[1]': 2 }
adder(2)            // 输出: 3    当前: cache: { '[1]': 2, '[2]': 3 }

In the memo function, use JSON.stringify to serialize the parameters passed to the adder function into strings, use it as the index of the cache, and pass the result of the add function as the index value to the cache, so that when the adder runs, if the passed parameters If it has been passed before, then the cached calculation result will be returned without further calculation. If the passed parameter has not been calculated, then fn.apply(fn, args) will be calculated and cached, and then the calculated result will be returned.

(4) Loop output problem

Finally, let’s look at a common loop output problem related to closures. The code is as follows:

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

The output of this code is 5 6s , so why are they all 6s? How can I output 1, 2, 3, 4, 5?

The first question can be considered in combination with the following two points:

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

So how to output 1, 2, 3, 4, 5 in sequence? 

1) Using IIFE  You can use IIFE (immediate execution function), when each for loop, pass the variable i at this time to the timer, and then execute it. 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) through such transformation, sequential output of serial numbers can be realized. Use the input parameter of the immediate execution function to cache the value of i in each loop. 

2) Use let in ES6  The newly added let definition variable in ES6 has revolutionized JS after ES6, allowing JS to have block-level scope, and the scope of code is executed in block-level units.

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

It can be seen that by redefining the i variable by using let to define the variable, the problem can be solved with the least modification cost.

3) The third parameter of the timer,  setTimeout, is a frequently used timer, and it has the third parameter. We often use the first two, one is a callback function, and the other is a timing time. SetTimeout can pass in countless parameters from the third input parameter position onwards. These parameters will exist as additional parameters of the callback function. Then combined with the third parameter, the adjusted code is as follows:

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

It can be seen that the passing of the third parameter can change the execution logic of setTimeout to achieve the desired result, which is also a way to solve the loop output problem.

 reference

Guess you like

Origin blog.csdn.net/qq_37308779/article/details/125923427