Use una pregunta de la entrevista para comprender cómo se forman los cierres

definición

MDN define los cierres como: Los cierres son funciones que pueden acceder a variables libres.

Entonces, ¿qué es una variable libre? Una variable libre es una variable que se usa en una función, pero no es ni un parámetro de función ni una variable local de la función.

A partir de esto, podemos ver que el cierre consta de dos partes: cierre = función + variables libres a las que puede acceder la función

Por ejemplo:

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

La función foo puede acceder a la variable a, pero a no es una variable local de la función foo ni un parámetro de la función foo, por lo que a es una variable libre. Entonces, la función foo + la variable libre a a la que accede la función foo constituye un cierre... ¡realmente lo es!

Así que en la "Guía definitiva de JavaScript" se dice: Desde un punto de vista técnico, todas las funciones de JavaScript son cierres.

Oye, ¿en qué se diferencia esto de los cierres que solemos ver y de los que hablamos? ?

No te preocupes, este es un cierre teórico. De hecho, hay un cierre práctico. Veamos la definición en el artículo sobre los cierres traducido por el Tío Tom:

En ECMAScript, el cierre se refiere a:

  1. Desde un punto de vista teórico: todas las funciones. Porque todos guardan los datos del contexto superior cuando se crean. Esto es cierto incluso para variables globales simples, porque acceder a variables globales en una función es equivalente a acceder a variables libres, y en este momento se usa el alcance más externo.
  2. Desde un punto de vista práctico: se consideran funciones de cierre las siguientes:
    1. Persiste incluso si se destruye el contexto en el que se creó (por ejemplo, la función interna regresa de la función principal)
    2. Las variables libres están referenciadas en el código.

A continuación, hablemos del cierre real.

analizar

Primero escribamos un ejemplo, el ejemplo sigue siendo de la "Guía definitiva de JavaScript", ligeramente modificada:

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

En primer lugar, debemos analizar los cambios en la pila de contexto de ejecución y el contexto de ejecución en este código.

Otro ejemplo similar a este código tiene un análisis muy detallado en "Contexto de ejecución en profundidad de JavaScript" . Si no comprende el siguiente proceso de ejecución, se recomienda leer este artículo primero.

Aquí hay un breve proceso de ejecución directamente:

  1. Ingrese el código global, cree un contexto de ejecución global e inserte el contexto de ejecución global en la pila de contexto de ejecución
  2. inicialización del contexto de ejecución global
  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] 是一样的道理。

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

Supongo que te gusta

Origin juejin.im/post/7230420475493974073
Recomendado
Clasificación