前言
在JavaScript中,闭包(Closure)是一个非常重要的概念,它涉及到函数和它们的作用域。闭包是指一个函数可以记住并访问它的词法作用域(lexical scope),即使这个函数在其词法作用域之外执行。
作用域链
在理解闭包之前,我们要明白什么是作用域链,让我们看看下面的代码
function bar(){
console.log(myname);
}
function foo() {
var myname = '彭于晏'
bar()
console.log(myname);
}
var myname = '吴彦祖'
foo()
在这段代码中,你觉得bar函数和foo函数打印出来的分别是什么呢,下面是代码的运行结果
让我们通过调用栈来分析一下这段代码,首先我们要创建全局执行上下文并编译执行
然后在全局执行foo函数时,要创建foo函数执行上下文,接着进行编译执行
在
foo函数执行bar函数时,要创建bar函数执行上下文,接着进行执行编译
那么接下来问题来了,在执行bar的时候,foo还没执行完,所以它的执行上下文还没有被销毁,那在bar()执行输出myname变量的时候,它会去哪里找,首先肯定是在自己的执行上下文里面去找,然后自己的执行上下文找不到,接下来去哪里找呢
然后我们要明白在所有的执行上下文里面都会有一个指向outer,我们可以把它称为指针,也可以称为外部引用,这个outer指明了该执行上下文的下一级是谁的,也就是指向我如果在自己的执行上下文找不到,我该去哪里找,而这个outer的指向就是词法作用域(函数定义在哪个域当中,这个域就叫该函数的词法作用域)
理解完了这个概念,我们知道bar和foo的词法作用域都是全局,所以bar在自己的执行上下文找不到,就会去全局找,因此bar执行输出myname变量就是吴彦祖,foo执行输出就是彭于晏
理解完词法作用域,我们来分析一下下面代码的函数的词法作用域分别是谁
let count = 1
function main() {
let count = 2
function bar() {
let count = 3
function foo() {
let count = 4
}
foo()
}
bar()
}
main()
首先肯定是创建全局执行上下文,然后在执行main函数时。里面声明了一个bar,并进行了调用,在执行bar函数时,对foo进行声明和调用,所以foo的outer指向为bar,bar的指向为main。foo的词法作用域是bar,bar的词法作用域是main。
像这样在在foo中使用变量,查找顺序为
foo=>bar=>main=>全局就是所谓的作用域链
总结
词法作用域(词法环境):函数定义在哪个域当中,这个域就叫该函数的词法作用域
v8在查找变量的过程中,顺着执行上下文中的outer指向查清一整根链,这种链状关系就叫作用域链
闭包
1,为什么会有闭包这个概念
function foo() {
function bar() {
var a = 1
console.log(b);
}
var b = 2
return bar
}
const baz = foo()
baz()
我们来分析一下这段代码,先创建全局执行上下文,然后进行编译执行,在执行foo函数时,开始创建foo函数执行上下文,然后进行编译执行,让我们来看看调用栈
然后我们可以看到foo函数就执行完毕了,然后就要进行销毁,接下来我们就要接着执行全局中的baz(),然后我们发现baz执行过程中输出了变量b,但是变量b并没有在bar中声明,只能在bar的词法作用域foo中寻找,但是foo已经被销毁了,按正常结果来说,foo已经被销毁了,那么就不存在变量b了,代码会报错,但实际是这样吗,让我们来看看代码的运行结果
b被正常打印出来了!
这就奇怪了,已经被销毁了怎么还能找到呢,这就要来看两条定理了;根据词法作用域的规则,内部函数总是可以访问其外部函数声明的变量,根据函数执行机制的规则,执行完的函数一定会被销毁。
因为这两条定律的冲突,所以在js这门语言中就出现了闭包这个概念
2.闭包的概念
上面代码中foo执行完毕后foo执行上下文会在调用栈里面被销毁,但是它会留下一个小区间,这个区间就被称为闭包,里面会存放在内部函数中要查找且可以查找到的已经销毁的外部函数变量
闭包是指在js中,根据词法作用域的规则,内部函数总是可以访问其外部函数声明的变量,
当通过调用一个外部函数返回的一个内部函数时,即使外部函数调用完了,但是内部函数引用了外部函数中的变量,那么这些变量依然需要保存在内部中,我们把这些变量的集合称为闭包
3,闭包的缺点
闭包会保留其创建时的词法环境(Lexical Environment),这意味着如果闭包长时间存在且频繁使用,会占用更多的内存,因为外部函数的变量不会被垃圾回收机制回收,直到闭包不再被引用。闭包中的对象可能会互相引用,从而导致循环引用问题。这可能会阻止垃圾回收机制回收这些对象,从而引发内存泄漏,
简单来说就是会多占用调用栈的内存
4,闭包的优点
可以实现变量的私有化,可以去封装一些模块
实例场景演示;创建一个累加器
function add() {
let num = 0
console.log(++num);
}
add()
add()
add()
这段代码的执行结果是什么呢,我们可以分析一下,在全局执行上下文中,执行add函数执行完毕后就会被销毁,因此三个add函数的执行结果都是一样的。
那我们如何实现累加效果呢,第一种就是将num变量定义在全局里面,这样确实是可以实现,但是在项目开发当中,如果定义全局变量就会有下面问题
-
命名冲突:
全局变量在项目的任何地方都可以被访问和修改,这增加了命名冲突的风险。如果不同的模块或函数使用了相同名称的全局变量,它们可能会互相干扰,导致不可预测的行为。
-
代码可维护性:
全局变量使得代码变得难以维护。因为它们可以在任何地方被访问,所以追踪一个全局变量的来源、用途和修改历史可能会变得非常复杂。这增加了代码出错的概率,并且使得修复错误变得更加困难。
-
代码可读性:
全局变量降低了代码的可读性。当函数或模块依赖于全局变量时,读者需要了解这些全局变量的定义和状态,才能完全理解代码的功能。这增加了理解代码的难度,特别是对于新加入项目的开发者来说。
所以我们尽量不要定义全局变量,那么第二种方法就是利用闭包
function add() {
let num = 0
return function foo() {
console.log(++num);
}
}
const res = add()
res()
res()
res()
在这段代码中add函数执行完毕后要进行销毁,但是呢函数内部的foo函数要访问add函数中的num,因此num就被放入包当中了,在执行res函数实际上就是执行add函数中返回的foo函数,然后再foo函数中只是对num进行了自增,num在包中并没有被销毁,也没有被重新定义,所以num只是在执行自增这一个操作从而达到累加的效果。
总结
在本文中,我们通过详细的代码示例和调用栈分析,深入探讨了闭包的概念、作用、缺点以及优点。我们还通过创建一个累加器的实例场景,展示了如何利用闭包来实现变量的私有化和模块的封装。
总的来说,闭包是JavaScript中一个非常重要的概念,它为我们提供了一种强大的方式来处理函数和作用域之间的关系。通过合理地使用闭包,我们可以编写出更加高效、可靠和易于维护的代码。希望本文能够帮助读者更好地理解和应用闭包,从而在JavaScript编程中取得更好的成果。