前言
在学习JavaScript的过程中,你是不是常常为闭包这一晦涩难懂的概念感到苦恼?甚至在看《你不知道的JavaScript》时艰难前行,那么今天我们就一起来聊聊闭包这个概念,闭包到底是个什么东西。
什么是闭包
通俗来讲,在JavaScript中,根据词法作用域的规则,内部函数可以访问定义它的作用域中的变量(即内层函数可以访问外层函数),当内部函数被拿到外部函数之外调用时,即使外部函数已经执行完毕,但是内部函数对外部函数中的变量依然存在引用,那么这些被引用的变量会以一个集合的方式保存下来,这个集合就叫做闭包。当然在这个闭包中outer指针也依旧会被保留下来。
举个例子
function foo() {
function bar() {
var age = 18
console.log(myName);
}
var myName = 'Tom'
return bar
}
var myName = 'jerry'
var fn = foo()
fn()
这里的内层函数被外层函数当成调用完它后的返回值,这里foo像不像一个父亲?而这个bar就像一个逆子,他本该被父亲掌控,最后却跑到了父亲之外,到底被不被调用,他父亲都不知道也无法掌控,可是你说foo执行完毕后foo算不算功德圆满呢?他确实是执行完毕了吧?那么既然执行完毕,按照JavaScript引擎规则调用栈在执行完毕后,foo的执行上下文是不是应该被销毁?画一个调用栈的图给你理解一下
- 首先代码
预编译发生在全局,创建全局上下文执行对象(GO:global object),找变量声明,将变量名作为GO的属性名,值为undefined,在全局找函数声明,将函数名作为GO的属性名,值为该函数体。如图所示,全局执行上下文中首先看到foo声明,值为这个函数体,然后就看见了myName变量声明,值为undefined,然后又看见了fn变量声明,值为undefined,预编译完成后,紧接着就执行代码,把jerry赋值给myName,然后执行调用foo函数 - 调用foo函数带来了foo函数的预编译。首先创建
foo的执行上下文对象(AO),用于存储函数内部的局部变量。然后找形参和变量声明,值为undefined,然后把形参和实参相统一,最后找函数声明,值为该函数体。如图所示:首先看到了bar函数声明,值为一个函数体,然后看见了myName变量声明,值为undefined。至此foo函数的预编译完成,紧接着就执行代码myName = Tom赋值语句,所以myName的值由undefined变为Tom然后foo全部执行完成带来了一个返回值,继续执行代码fn = foo(),因此全局执行上下文对象中的fn由undefined变为了bar,最终执行fn函数。 - 执行fn函数带来了fn函数的预编译,那么就会创建一个
bar执行上下文对象,在这个执行上下对象中首先看到了age的声明,值为undefined,然后预编译完成后继续执行代码,就执行赋值语句,age的值进而改成18。 - 在foo函数执行完毕后它已然没用了,照理
垃圾回收机制该给他这个执行上下文对象销毁了,否则如果所有的对象都用完了但是这个调用栈结构中都不去销毁的话,岂不是人满为患了?(栈溢出),但是前文说的,它的这个bar函数(内层函数)不受外层函数的控制了,被拿到全局中而不是外层函数中调用了,但是在这个bar函数中依然需要使用外层函数中的一些变量,myName,如果我们简单的把foo执行上下文全部销毁了,那bar的myName不就没办法找外层函数要了吗?因此在JavaScript引擎中就会发现这个问题,并留下一个小背包,将内层函数依然需要用到的变量给留下来,等到哪天全局中真的调用了内层函数需要用这些变量的时候,它就能够找到在外层函数中本该在那个位置的变量了。这个小背包就是我们说的闭包。 - 至此你应该彻底理解了到底什么是闭包,
很多书籍上说闭包的概念,讲的都是闭包是怎么发生的,而真正意义上的闭包应该是这个小背包。 - 在图片中,我们能够看到一个outer指针,沿着这个outer指针就形成了一个链条,便于在寻找变量等时能够沿途搜索,这个链条就是我们说的作用域链,那么这个
outer是如何指向的呢?规则是:我的词法作用域在哪里我就指向哪里,通俗来讲就是,谁声明了我,我的词法作用域就是谁,谁声明了我,我就指向谁,在上述例子中,在全局执行上下文中,声明了foo函数,那么foo的执行上下文对象就应该指向全局执行上下文,当然,在最底层,全局执行上下文的outer再指向谁已经没有意义了,因此这个outer指针为一个空指针。 - 那么这个闭包的outer指针又应该指向谁呢?毋庸置疑,foo执行上下文就像它的父亲,而这个闭包就是父亲留下来的一个小背包,那父亲指向哪里,闭包自然指向哪里。
至此,你应该彻底了解了什么是闭包,outer指向问题以及作用域链、执行上下文的相关概念
闭包的作用
- 数据隐藏和封装:保护内部数据不被外部随意修改。
- 保持状态:使函数内部的变量在函数执行完后仍然能被访问和操作。
- 实现模块和私有方法:模拟私有变量和方法的效果。
- 回调函数:在异步编程中经常使用。
闭包的缺陷
- 可能导致内存泄漏:如果闭包中引用的对象没有被正确释放,可能会占用过多内存。
- 增加复杂性:过度使用闭包可能使代码结构变得复杂,增加理解和维护的难度。
闭包应用例子
写一个自增计数器,让我每次调用时能够从1,变成2,再调用时从2变成3,以此类推,每次加1.
function add() {
let count = 0;
function fn() {
count++
return count;
}
return fn;
}
var res = add();
console.log(res());
console.log(res());
console.log(res());
在我们对自增函数进行封装时,如果把什么封装都要把变量封装到全局去,那长此以往岂不是在堆屎山?因此我们想把这个count变成一个块级作用域,但是又想要访问到这个count(保留count变量),正常来说当add函数执行完毕,这个执行上下文就应该销毁了,那么count变量只能去全局中找,但是我们要实现++,并且想保留count变量,这就可以应用闭包。
结语
闭包是函数与作用域的奇妙结合体,具有重要作用,如数据隐藏、保持状态和实现模块等;但同时也存在弊端,如可能导致内存泄漏和增加代码复杂性。在使用闭包时,需要谨慎权衡其利弊,以实现更高效、可靠的编程。对闭包的深入理解和恰当运用,能提升我们的编程能力和代码质量。