¿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
son
puede regresar y ejecutarse en cualquier momento. - El alcance léxico determina : una función
son
puede acceder aparent
las variables de la funciónn
.
No pienses en otros conceptos ahora.
Intuitivamente pensamos que: son
ejecución , acceso a la ejecuciónparent
del alcance de la función .n
console.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入栈,
复制代码
要让 son
可以在 parent
出栈后依然能访问到 n
。
需要个新技术来解决这个问题。
闭包诞生
为了解决上述问题( 词法作用域 + 函数是一等公民 + 函数调用栈 正常运行)。诞生了闭包这个概念。
闭包其实是一种技术:在函数调用栈的基础上,同时实现函数是一等公民、词法作用域。
闭包的具体实现
核心原理:将闭包所需的数据,都存储到堆(Heap)上。
堆(Heap)的数据,是不会随着函数调用结束而回收的。
上面的举例函数,在 chrome
中的闭包信息:
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)上。
- 闭包的特性:
- 保存:闭包将局部变量的生命周期拉长,不在随着函数调用结束而回收。
- 保护:不会成为全局变量、也不会收到外部影响。
- 可以构建私有变量。
- 闭包的应用
- 模块
- 私有属性
- 高阶函数、有状态的函数
- 闭包的缺点
- 内存泄漏