likes
comments
collection
share

熬夜爆肝,只为打造通俗易懂的闭包解析💥

作者站长头像
站长
· 阅读数 14

闭包

熬夜爆肝,只为打造通俗易懂的闭包解析💥

导语

人人都说闭包难,然而不是人人都看了这篇文章。本文将带您深入探索————闭包。

一些底层逻辑

变量声明提升和函数声明整体提升:

执行结果:

我们知道代码在执行前会进行编译,JS这门语言与其他不同,它是边执行边编译,在代码执行前一刻进行编译,那么在引擎眼里,它会将代码看成下面这个样子:


调用栈:

定义:调用栈是一种用来管理函数调用关系的数据结构,当一个函数执行完毕后,它的执行上下文就会出栈。

将这个代码拿到浏览器执行的时候会报出这样的错误:

Uncaught RangeError: Maximum call stack size exceeded

浏览器说你的栈溢出了,所以我们就可以初步理解成,引擎在编译代码的时候,存在一个栈结构,而且栈的空间是有限的。

下面我来详细介绍一下调用栈在代码中是如何实现的:

var a =2 
function add(b,c){
    return b + c
}
add()

function addAll(b,c){
    var d =10
    var result = add(b,c)
    return a+result+d
}
addAll(3,6)

21

在这段代码中,我们在addAll函数中调用了add函数,那么在调用栈中怎么实现的呢,我用图形解释给大家。

全局编译时会创建出一个调用栈,然后全局执行上下文入栈,开始执行,变量a的值从undefined变成2

熬夜爆肝,只为打造通俗易懂的闭包解析💥

然后执行到addAll前一刻,开始编译addAlladdAll执行上下文入栈,然后执行给变赋值,

熬夜爆肝,只为打造通俗易懂的闭包解析💥

执行到add(b,c)的前一刻,开始编译addadd执行上下文入栈,给bc传值,然后add函数执行完毕,add执行上下文出栈。

熬夜爆肝,只为打造通俗易懂的闭包解析💥

接着执行addAll后面的代码,result拿到值,addAll函数return 21,最后addAll函数执行完毕,出栈,addAll出栈后,全局代码执行完毕,全局执行上下文再出栈。

熬夜爆肝,只为打造通俗易懂的闭包解析💥

以上就是调用栈的概念,就是引擎在维护函数执行过程中的关系。


作用域链

定义:通过词法作用域来确定某作用域的外层作用域,查找变量由内而外的这种链状关系,叫做作用域链

通过下面这个例子我们聊聊作用域的概念:

function bar(){
    console.log(myName);
}

function foo(){
    var myName = '龙龙'
    bar()
}

var myName = '君君'
foo()

君君

foo调用了barbar的词法作用域没有关系,bar声明在了全局,所以bar的词法作用域在全局,所以在调用bar打印myName的时候,在bar自己的作用域内找不到myName的时候,就会去词法作用域找,而bar的词法作用域在全局而不再foo内,所以打印结果为"君君"

下面我们在调用栈里面看看:

按照我们之前的理解,在调用栈内,三个执行上下文依次入栈,在bar要打印myName,自己的作用域内没有只能去外面找,那么是去foo还是全局呢,按我们之前的理解,肯定是直接往下找,用foo里的myName,可是结果告诉我们,是直接跳到了全局,用全局的myName

熬夜爆肝,只为打造通俗易懂的闭包解析💥

那么我们可以猜测它这个调用栈里面肯定还有什么能够标记性的东西,其实在每个变量环境里面都有一个有指向性的outer,用于指向自己外层的作用域,全局执行上下文中的outer的值为null,因为全局已经是最外层,foo执行上下文的outer指向全局,bar执行上下文的outer也是指向全局的。

熬夜爆肝,只为打造通俗易懂的闭包解析💥

所以我们就可以解释上面的打印结果,我们把这个查找的链条称为作用域链


var arr= []

for(var i=0; i<10;i++){
    arr[i]= function(){
        console.log(i);
    }
}

for(var j=0; j<arr.length;j++){
    arr[j]();
}

在上面的代码中,我们在数组里放了十个函数,然后用另一个循环来调用它,十个打印结果是什么呢?

10 10 10 10 10 10 10 10 10 10

我们在全局用var声明了变量i,将10个i存进数组中,这时候的i并没有直接被打印,因为还是还没被调用,当循环存储i加到了10之后,循环结束,最后再用for循环调用数组中的函数,这时候的i就都是10

思考:那么这行代码如果我就想要打印结果为0~9,那该如何实现呢?

最优雅的解决方案就是将var改为let,因为let{}会形成块级作用域,函数在执行的时候,它的词法作用域在花括号内,也就是下面这个区域,在函数调用的时候,函数体里面找不到i,就会去外层作用域里找,for循环了十次,就会创建十个块级作用域来存放这个i,所以打印的就是0~9

for(let i=0; i<10;i++){
    arr[i]= function(){
        console.log(i);
    }
}

思考:那么请问还有别的办法让它打印0~9吗?

我们先把这个问题丢在这儿,来聊一聊今天的主角,等会回过头来看看能不能解决。

闭包

定义:在JS中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量的,当内部函数被返回到外部函数之外时,即使外部函数执行结束了,但是内部函数引用了外部函数的变量,那么这些变量依旧会被保存在内存中,我们把这些变量的集合称为闭包

我们用例子来讲解一下:

function foo(){
    var myName = '旭旭'
    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开始调用,于是foo执行上下文入栈,var声明的变量放在变量环境,let声明的变量放在词法环境。

熬夜爆肝,只为打造通俗易懂的闭包解析💥

foo开始执行,给变量赋值,然后foo调用完毕,出栈。

熬夜爆肝,只为打造通俗易懂的闭包解析💥

bar调用setName,setName执行上下文入栈。

熬夜爆肝,只为打造通俗易懂的闭包解析💥

然后给newName赋值,然后我们发现foo都出栈了,哪里来的值?

其实这个地方有个特殊的操作,foo执行上下文确实出栈了,但是它留下了一个小包裹,即test1=1myName='旭旭'

熬夜爆肝,只为打造通俗易懂的闭包解析💥

解释一下: setNamegetName是定义在foo里面,并不是定义在innerBar对象里面,因为对象不能形成作用域,所以setNamegetName能访问foo作用域里面的变量,可是setNamegetName不是在foo里面调用的,它们是拿出来调用的,在全局被调用的。一个函数在调用完毕后执行上下文一定要出栈,但是现在foo函数体内的test1myName的声明是在函数体内,但是没有在函数体内调用,foo函数不能确定它们何时被调用,所以在foo出栈的时候,会将这两个变量留下来,留下来的这个小包裹就是闭包

在JS中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量的,当内部函数被返回到外部函数之外时,即使外部函数执行结束了,但是内部函数引用了外部函数的变量,那么这些变量依旧会被保存在内存中,我们把这些变量的集合称为闭包

我们再用一道题来强化一下:

function a(){
    function b(){
        var bbb=234;
        console.log(aaa);
    }
    var aaa=123;
    return b;
}
var c =a()
c()

这里我们将a内的函数b返回到a外面调用,b这个函数里面用到了a的变量,这就形成了闭包,我们在调用栈里面来看看:

首先全局执行上下文入栈,声明function a()和变量c,然后执行a前一刻开始编译,a的执行上下文入栈,里面声明了function b()和变量aaa,在执行的时候aaa被赋值为123,随着return结束,a的执行上下文出栈。

熬夜爆肝,只为打造通俗易懂的闭包解析💥

a执行完毕,按道理来说a里面的函数b也应该出栈,但是b被抛到a函数的外面调用,导致a的执行上下文无法完全被销毁,而是留下了一个包裹,这个包裹里面装的是b这个函数所要用到的变量aaa,这就是我们说的闭包

熬夜爆肝,只为打造通俗易懂的闭包解析💥

闭包出现就代表了a这个函数彻底执行完毕,那我们接着往后看,函数c调用,c的执行上下文入栈,c也就是我们的函数b,声明变量bbb并赋值,随后console.log(aaa),发现b里面找不到变量aaa。于是去自己的外层作用域找,也就是去a里面找,最终在小包裹里面找到了aaa=123,所以最终打印结果为123

所以我们可以总结一下闭包的优缺点:

优点:

  • 变量私有化,闭包可以避免变量写在全局,保存变量不被销毁。

缺点:

  • 内存泄露,闭包会导致栈空间占用不足。

这里就不展开讲了,我们再回头看一下开头那道题:

var arr= []

for(var i=0; i<10;i++){
    arr[i]= function(){
        console.log(i);
    }
}

for(var j=0; j<arr.length;j++){
    arr[j]();
}

我们想,闭包的作用是不是保存变量不被销毁,让它一直留在调用栈当中,如果我们有办法让调用栈里面放十个小包裹,每个小包裹里面放一个i,等到后面调用的时候,你去自己的小包裹里面找自己对应的i不就好了?

我们直接把for循环里面的function放到一个自循环函数a里面就可以实现:


var arr= []
for(var i=0; i<10;i++){
    (function a(j){
        arr[i]= function(){
            console.log(j);
        }
    })(i)
}

for(var j=0; j<arr.length;j++){
    arr[j]();
}

a是一个立即执行函数,它接受循环变量 i 作为参数,并且立即执行。这个函数将传入的 i 的值存储在闭包内部,并且将一个新的函数赋值给数组 arr 的相应位置。这个新的函数会打印传入的参数 j 的值。

最后的打印结果也如我们所愿。

至此,本文对闭包的讲解已经结束了,不管您是小白还是大佬,如果这篇文章对你有帮助,可以赏个小心心嘛,或者你觉得哪里还有不足,哪里还有欠缺,欢迎私信和评论区留言,我都会一一回复的❤!

最后

技术小白记录学习过程,有错误或不解的地方还请评论区留言,如果这篇文章对你有所帮助请 “点赞 收藏+关注” ,感谢支持!!