¿Qué es un cierre? ¡Comencemos con por qué hay cierres!

¿Qué es un cierre? Comencemos con por qué hay cierres.

El origen de los cierres

Wikipedia: El concepto de cierres se desarrolló en la década de 1960 para la evaluación automática de expresiones usando cálculo lambda, y se implementó completamente por primera vez en el lenguaje de programación PAL en 1970 para admitir funciones de primera clase con alcance léxico.

Palabras clave:

Alcance léxico : el alcance de una función se determina cuando se define la función (es decir, donde se escribe el código).

Funciones de primera clase ( las funciones son ciudadanos de primera clase ): las funciones se pueden usar como argumentos de funciones, devolver valores de funciones, asignarse a variables o almacenarse en estructuras de datos.

Ejemplo

Escriba una función JS para ilustrar

function parent() {
  let n = 1
  function son() {
    // 词法作用域: 可以访问到n
    console.log(n);
  }
  return son // 函数是一等公民: 可以被返回
}

let son = parent()
son()// 函数是一等公民: 返回的函数可以执行。
复制代码
  • La función es un ciudadano de primera clase . Se determina que la función sonpuede regresar y ejecutarse en cualquier momento.
  • El alcance léxico determina : una función sonpuede acceder a parentlas variables de la función n.

No pienses en otros conceptos ahora.

Intuitivamente pensamos que: sonejecución , acceso a la ejecuciónparent del alcance de la función .nconsole.log

¿Es correcto? correcto.

¿Qué pasa con el concepto de cierres?

¿Lo encontré? No conocemos el concepto de cierre, pero no lo afecta en absoluto, ¡sabemos la ejecución del código!

¿Por qué existe un concepto de cierres?

Primero hablemos de un concepto: pila de llamadas de función

Hable brevemente sobre el flujo de ejecución de una función en la pila de llamadas de funciones.

  • Las funciones se ejecutan en la pila y utilizan la memoria de la pila .
  • Las funciones almacenan variables locales , datos de tipo básico e información de ejecución de funciones en la memoria de la pila .
  • Después de ejecutar la función, se extrae de la pila.

Después de abrir la pila, no se puede acceder a las variables locales y los datos de tipo básico guardados en la pila y los datos no existen.

por ejemplo

Qué sucede con la ejecución si no hay cierre.

// 在没有闭包的调用栈中执行
// 下面有图,看图理解
function parent() {
  let n = 1 // 局部变量n,基本数据类型1
  function son() { // 局部变量son

    // son执行时,parent已经出栈,变量n访问不到了
    // 报错!
    console.log(n);
  }
  return son  
  // parent出栈
}

let son = parent() // parent入栈
son()// son入栈,
复制代码

Untitled Diagram.drawio (5).png 要让 son 可以在 parent 出栈后依然能访问到 n

需要个新技术来解决这个问题。

闭包诞生

为了解决上述问题( 词法作用域 + 函数是一等公民 + 函数调用栈 正常运行)。诞生了闭包这个概念。

闭包其实是一种技术:在函数调用栈的基础上,同时实现函数是一等公民、词法作用域。

闭包的具体实现

核心原理:将闭包所需的数据,都存储到堆(Heap)上。

堆(Heap)的数据,是不会随着函数调用结束而回收的。

Untitled Diagram.drawio (4).png

上面的举例函数,在 chrome 中的闭包信息:

Untitled.png

v8将函数闭包所需的数据,构成一个Closure对象放在堆上,然后函数引用这个对象。

再次调用时,函数会去访问Closure对象里的数据。

(深入原理请看一文颠覆大众对闭包的认知

闭包的特性:保存、保护变量

根据闭包的实现,我们可以看出,Closure 是一个私有的、不影响全局的对象。

从而有了以下特性:

  • 保存:闭包将局部变量的生命周期拉长,不在随着函数调用结束而回收。
  • 保护:不会成为全局变量、也不会收到外部影响。
    • (我认为是因为词法作用域的作用,也算闭包的一部分)

私有变量,很重要。

闭包的应用

模块

我们要提供一个模块供别人使用,要解决命名空间污染的问题。

命名空间污染:模块要用多个变量,我们希望变量不影响全局,全局也不影响我们的变量。

IIFE + 闭包 即可实现。


// 闭包实现模块化。
var moduleA = (function (global, doc) {
  var methodA = function() {};
  var dataA = {};
  return {
    methodA: methodA,
    dataA: dataA
  };
})(this, document);

复制代码

现代JS提供ESM,原生支持模块。

模拟私有属性

// 模拟私有属性
function Test(name) {
  let _name = name;
  return {
    getName: function () {return _name;},
  };
}
let obj = new Test('z');
obj.getName(); // z
obj._name // undefined
复制代码

现代JS,class支持私有属性(操作符#)。

高阶函数、有状态的函数

例如:高阶函数、科里化、节流防抖(等要判断状态的函数)

// 节流
function throttle(fn, timeout) {
    let timer = null
    return function (...arg) {
        if(timer) return
        timer = setTimeout(() => {
            fn.apply(this, arg)
            timer = null
        }, timeout)
    }
}
复制代码

其实都应该算函数式编程的高阶函数。

闭包的缺点

内存泄漏

数据被闭包对象 Closure 引用,无法被释放回收。

闭包函数又在多个地方被引用,导致数据引用复杂,容易发生内存泄漏问题。

什么是闭包?

闭包(多种说法):

  • MDN:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被用包围),这样的组合就是闭包closure)。
    • 闭包让你可以在一个内层函数中访问到其外层函数的作用域。
    • 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
  • Rust语言:闭包允许捕获调用者作用域中的值。
  • 维基百科:闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。
  • 闭包其实是一种技术:在函数调用栈的基础上,同时实现函数是一等公民、词法作用域。

闭包的实现有多种方式,所以解释就有差异。

面试就回答MDN的说法就好。

总结

  • 闭包解决了什么问题?
    • 函数调用栈的基础上,同时实现函数是一等公民、词法作用域。
  • JS中的闭包是什么?(MDN的解释)
    • 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被用包围),这样的组合就是闭包closure)。
    • 闭包让你可以在一个内层函数中访问到其外层函数的作用域。
    • 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
  • 闭包的实现:
    • 原理:将闭包所需的数据,都存储到堆(Heap)上。
    • V8会将闭包所需的数据,存在函数的[[Scope]]Closure 对象上,这个对象在堆(Heap)上。
  • 闭包的特性
    • 保存:闭包将局部变量的生命周期拉长,不在随着函数调用结束而回收。
    • 保护:不会成为全局变量、也不会收到外部影响。
    • 可以构建私有变量
  • 闭包的应用
    • 模块
    • 私有属性
    • 高阶函数、有状态的函数
  • 闭包的缺点
    • 内存泄漏

参考文献

  1. MDN
  2. 维基百科
  3. JavaScript深入之词法作用域和动态作用域
  4. 深入理解JavaScript闭包(closure)
  5. 一文颠覆大众对闭包的认知(深讲)

Supongo que te gusta

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