Use an interview question to understand how closures are formed

definition

MDN defines closures as: Closures are functions that can access free variables.

So what is a free variable? A free variable is a variable that is used in a function, but is neither a function parameter nor a local variable of the function.

From this, we can see that the closure consists of two parts: closure = function + free variables that the function can access

for example:

var a = 1;
function foo() {
    console.log(a);
}
foo();
复制代码

The foo function can access the variable a, but a is neither a local variable of the foo function nor a parameter of the foo function, so a is a free variable. Then, the function foo + the free variable a accessed by the foo function constitutes a closure...it really is!

So in the "JavaScript Definitive Guide" it is said: From a technical point of view, all JavaScript functions are closures.

Hey, how is this different from the closures we usually see and talk about! ?

Don't worry, this is a theoretical closure. In fact, there is a practical closure. Let's take a look at the definition in the article about closures translated by Uncle Tom:

In ECMAScript, closure refers to:

  1. From a theoretical point of view: all functions. Because they all save the data of the upper context when they are created. This is true even for simple global variables, because accessing global variables in a function is equivalent to accessing free variables, and the outermost scope is used at this time.
  2. From a practical point of view: the following functions are considered closures:
    1. It persists even if the context in which it was created is destroyed (e.g. inner function returns from parent function)
    2. Free variables are referenced in the code

Next, let's talk about the actual closure.

analyze

Let's write an example first, the example is still from "JavaScript Definitive Guide", slightly modified:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
var foo = checkscope();
foo();
复制代码

First of all, we need to analyze the changes in the execution context stack and execution context in this code.

Another example similar to this code has a very detailed analysis in "JavaScript In-Depth Execution Context" . If you don't understand the following execution process, it is recommended to read this article first.

Here is a brief execution process directly:

  1. Enter the global code, create a global execution context, and push the global execution context into the execution context stack
  2. global execution context initialization
  3. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
  4. checkscope 执行上下文初始化,创建变量对象、作用域链、this等
  5. checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
  6. 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈
  7. f 执行上下文初始化,创建变量对象、作用域链、this等
  8. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出

了解到这个过程,我们应该思考一个问题,那就是:

当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢?

以上的代码,要是转换成 PHP,就会报错,因为在 PHP 中,f 函数只能读取到自己作用域和全局作用域里的值,所以读不到 checkscope 下的 scope 值。然而 JavaScript 却是可以的!

当我们了解了具体的执行过程后,我们知道 f 执行上下文维护了一个作用域链:

fContext = {
    Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
复制代码

对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。

所以,让我们再看一遍实践角度上闭包的定义:

  1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  2. 在代码中引用了自由变量

在这里再补充一个《JavaScript权威指南》英文原版对闭包的定义:

This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature.

闭包在计算机科学中也只是一个普通的概念,大家不要去想得太复杂。

必刷题

接下来,看这道刷题必刷,面试必考的闭包题:

var data = [];
for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}
data[0]();
data[1]();
data[2]();
复制代码

答案是都是 3,让我们分析一下原因:

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}
复制代码

当执行 data[0] 函数的时候,data[0] 函数的作用域链为:

data[0]Context = {
    Scope: [AO, globalContext.VO]
}
复制代码

data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。

data[1] 和 data[2] 是一样的道理。

所以让我们改成闭包看看:

var data = [];
for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}
data[0]();
data[1]();
data[2]();
复制代码

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}
复制代码

跟没改之前一模一样。

当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:

data[0]Context = {
    Scope: [AO, 匿名函数Context.AO globalContext.VO]
}
复制代码

匿名函数执行上下文的AO为:

匿名函数Context = {
    AO: {
        arguments: {
            0: 0,
            length: 1
        },
        i: 0
    }
}
复制代码

data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以打印的结果就是0。

data[1] 和 data[2] 是一样的道理。

你如果仔细的看完上面的分析过程,应该对闭包是怎么形成的有个具体的认知了吧!!!

Guess you like

Origin juejin.im/post/7230420475493974073