前言
闭包是JavaScript中一个重要的概念,它允许函数访问其创建时的作用域之外的变量。闭包在很多情况下都非常有用,包括封装私有变量、实现模块化、保存状态和创建高阶函数等。对于小白来说,闭包是js中很难的一个知识点,接下来,让我们一起深入学习闭包。
什么是闭包?
在js中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量的,当内部函数被返回到外部函数之外时,即使外部函数执行结束了,但是内部函数引用了外部函数的变量,那么这些变量依旧会被保存在内存中,我们把这些变量的集合称为闭包。
让我们用代码来看看,什么是闭包。首先我们需要了解的是调用栈和预编译。
调用栈
用来管理函数调用关系的一种数据结构,当一个函数执行完毕以后,它的执行上下文就会出栈。
预编译
预编译发生在全局 (三部曲)
- 创建GO 对象 (Global Object)全局执行上下文
- 找有效标识符(变量声明),将变量声明作为AO的属性名,值为undefined
- 在全局找函数声明,将函数名作为GO对象的属性名,值赋予函数体
预编译发生在函数执行之前 (四部曲)
- 创建AO对象(Action Object) 上下文对象 记录函数中有哪些标识符
- 找有效标识符(形参和变量声明),将变量声明和形参作为AO的属性名,值为undefined
- 将实参和形参值统一
- 在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体
理解闭包
接下来,我们结合调用栈,预编译,通过代码的画图来理解什么是闭包。
首先我们来看第一个例子
js
showName()
console.log(myName); //undefined
var myName = "dante"
function showName(){
console.log('周杰伦'); //周杰伦
}
上述代码中,当v8引擎在预编译的过程中,会先创建一个全局执行上下文存放到调用栈中,然后查找到一个标识符myName ,并赋值为undefined。查找到一个函数声明showName,值为function。在该函数执行前,创建一个函数上下文存放在调用栈中,并查找有效标识符,以及函数声明。上述代码调用栈中的执行上下文如所示:
预编译之后,代码从上到下执行,首先调用showName()这个函数,打印周杰伦后,被销毁出栈。执行console.log(myName)时,myName值为undefined,所以输出undefined,继续执行,将dante的值赋给myName。执行结束出栈。
现在我们来看如下一段代码,根据上面预编译和执行栈的规则来看看,会输出什么。
js
function foo() {
var myName = 'dante'
let test1 = 1
let test2 = 2
var innerBar = {
getName: function() {
console.log(test1)
return myName
},
setName: function(newName) {
myName = newName
}
}
return innerBar
}
var bar = foo()
bar.setName('杰伦')
console.log(bar.getName())
输出的结果是1,周杰伦。我们通过调用栈来理解一下,为什么是这个结果。
如图所示,首先全局上下文入栈,全局上下文变量环境中有函数体foo,变量bar,**outer = null(每个执行上下文中都有outer,其作用是指明当前词法作用域在哪)。**执行到16行代码时,foo执行上下文入栈,并执行foo中的代码后如上图所示,当foo执行完毕后,foo执行上下文出栈,接下来执行bar.setName(),setName执行上下文入栈,其变量环境中包含newName,以及outer。编译setName中的代码,发现变量myName在哪呢?无法找到,那为什么getName()可以输出周杰伦。
foo确实是出栈了,但是其留下了一个小背包,包含了test = 1, myName = 周杰伦,这个小背包就叫做闭包。
闭包的缺点
尽管JavaScript闭包是一个强大的功能,但它也有一些潜在的缺点,需要开发者注意:
- 内存占用:闭包会保留对外部函数的作用域的引用,这可能导致内存占用增加,特别是在创建大量闭包时。如果不小心处理闭包,可能会导致内存泄漏问题,因为被闭包引用的变量不会被垃圾回收。
- 性能开销:由于闭包涉及访问外部作用域,它们通常比普通函数调用要慢一些。这种性能开销在某些性能敏感的应用中可能会有所影响。
- 难以理解和调试:复杂的嵌套闭包结构可能难以理解和调试。追踪闭包中的变量引用和作用域链可能会导致代码维护困难。
- 变量共享:当多个闭包共享相同的外部变量时,可能会导致意外的行为,因为一个闭包的修改会影响其他闭包的状态。这可能会引发不可预测的错误。
- 过度使用:有些开发者可能过度使用闭包,导致不必要的复杂性和性能问题。在简单情况下,使用普通函数可能更合适。
- 可维护性问题:在复杂应用中,滥用闭包可能导致代码难以维护。正确使用闭包需要良好的代码组织和文档,以避免混乱和混乱的代码。
要避免这些潜在问题,开发者应当谨慎使用闭包,并确保了解它们的工作原理以及何时使用它们。使用适度的闭包来实现特定目标,同时注意内存管理和性能问题,可以减轻闭包带来的潜在缺点。
总结
JavaScript闭包(Closure)是一个重要概念,它包含了函数和其相关的词法环境,允许函数访问在其外部定义的变量。以下是对JavaScript闭包的总结:
-
定义:闭包是指函数和其包含的词法环境的组合,使函数能够访问外部作用域的变量。
-
工作原理:当函数定义时,它会创建一个闭包,保存了函数内部的代码和它所能访问的外部变量。这些外部变量在函数执行时仍然可用。
-
访问外部变量:闭包可以访问外部函数的局部变量,即使外部函数已经执行完成。这使得闭包可以"记住"外部环境的状态。
-
应用:
- 封装:使用闭包可以创建私有变量和函数,实现封装和模块化。
- 回调函数:常用于处理异步操作、事件处理和模块化开发,通过闭包可以传递函数和数据。
- 高阶函数:能够返回函数的函数,经常使用闭包来创建高阶函数。
-
内存管理:需要注意,闭包中的变量会一直保存在内存中,直到不再被引用。滥用闭包可能导致内存泄漏,因此需要小心处理。
-
实例:一个典型的闭包示例是将内部函数从外部函数返回,然后在外部函数执行后调用内部函数,它仍然可以访问外部函数的变量。