初学者也能理解的闭包(JS)——涵盖执行期上下文,预编译,作用域链,立即函数

本文深入探讨JavaScript的执行上下文、预编译机制,包括函数预编译、全局预编译和作用域链。通过实例解析执行上下文如何创建AO/GO对象,以及闭包的概念和内存泄漏。同时介绍了立即执行函数在处理闭包问题上的应用。
摘要由CSDN通过智能技术生成

执行期上下文

定义:当函数执行时,会创建一个称为执行期上下文(AO对象)的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,它所产生的执行上下文被销毁。(⭐重点,后面要考的!!!)

预编译

预编译,就是编译前的过程。可以理解为:预编译就是做到函数声明整体提升,变量声明提升

详细的预编译知识点可以看JavaScript之预编译学习(内含多个面试题),这里只提及关键部分。

函数预编译

关于函数预编译我们只要学会怎么确定执行期上下文——AO(Activation Object)对象就行了。可能有的小伙伴还是对前面执行期上下文的定义有疑问:执行上下文在函数执行中起啥作用,多个执行上下文具体是指啥,独一无二又是怎么体现的呢?这个部分会解开你的疑惑。

如果我们要确定下面这个函数的执行期上下文和打印值,应该怎么做呢?

    function test(a){
    console.log(a);
    var a=123;
    console.log(a);
    function a(){}
    console.log(a);
    var b=function() {}
    console.log(b);
    function d() {}
   }
   test(1);
  1. 创建AO对象
    AO:{}
  1. 将形参和变量声明找出来作为AO的属性名,并且统一赋值undefined
    AO: {
        a: undefined,
        b: undefined
    }
  1. 将实参值赋值给形参
    AO: {
        a: 1,
        b: undefined
    }
  1. 从函数里找到函数声明,函数声明中的函数名作为AO的属性名,函数声明中的函数体作为这个属性名的值
    AO: {
        a: function a(){},  //因为函数声明a和变量声明a重名,对变量声明a进行覆盖
        b: undefined, //如果以为此时b:function() {} ,就要理解函数声明!=变量声明
        d: function d() {}
    }

至此,AO对象已经确立好了。那么编译里怎么用到这个AO对象呢?你可以试着写出那些被打印出来的值,最终答案是这个:

或许和你的预期不一样?那是因为没有正确使用上AO对象。让我们重新分析一下:

    function test(a){
        console.log(a);  //我们从AO里找有没有a,有的,值是function a(){},那么打印结果就是function a(){}
        var a=123;  //分为两部分:变量声明a,将a赋值为123。变量声明a我们在确定AO对象就已经看过了,忽略它。a被赋值123,那我们就修改AO中a的值为123
        console.log(a);  //我们从AO里找a的值,发现是123,那么就打印123
        function a(){}  //这条我们在确定AO对象就已经看过了,忽略它往下走
        console.log(a);  //AO里a的值仍为123,打印123
        var b=function() {}  //分为两部分:变量声明b,将b赋值为function() {}。变量声明b我们在确定AO对象就已经看过了,忽略它。b被赋值为function() {},因此AO中b的值不再是undefined,改为function() {}
        console.log(b);  //AO里b的值是function() {},那就输出function() {}
        function d() {}  //这条我们在确定AO对象就已经看过了,忽略它。分析到此结束。
   }
   test(1);  //调用函数test,那我们开始分析这个函数叭⬆

一波分析下来,相信你对执行上下文有了更深的理解。另外,在确立AO对象与console.log()输出时,有两个小技巧:

  • 函数声明与变量声明同名时,最终AO对象确定时只用管函数声明(因为函数声明在第四步,会覆盖之前同名的变量声明)
  • console.log(xx)的前面一条代码如果是对变量xx赋值,那么输出的就是这个值

全局预编译

与函数预编译类似,关于全局预编译,只要知道怎么确定全局对象——GO(Global Object)对象就行了。(注:window对象和GO对象是同一个对象的不同名字,也就是说:window === GO)

GO对象的确立方法也和AO对象确立方法类似,只是修改了步骤二——形参去掉,少了步骤三(因为找的是全局变量),康康下面这个例子:

    console.log(a);
    var a=1;
    console.log(a);
    function test(a){
        console.log(a);
        var a=123;
        console.log(a);
        function a(){}
        console.log(a);
        console.log(b);
        var b=function() {}
        console.log(b);
   }
   test(2);

我们开始确定GO对象叭:

  1. 创建GO对象
    GO:{}
  1. 将变量声明找出来作为GO的属性名,并且统一赋值undefined
    GO:{
        a:undefined,
        c:undefined
    }
  1. 从函数里找到函数声明,函数声明中的函数名作为GO的属性名,函数声明中的函数体作为这个属性名的值
    GO:{
        a:undefined,
        c:undefined,
        test:function test() {...}
    }

为了解题,这里把AO对象也写出来:

    AO:{
        a: function a() {},
        b: undefined
    }

GO对象在编译时起到什么作用呢?看看解题过程叭

    console.log(a);  //GO里a的值是undefined,那么打印结果就为undefined
    var a=1;  //两部分:变量声明a,将a赋值为1。变量声明a在确定GO对象时看过了,忽略;a被赋值1,GO里a的值也从undefined变成1
    var c=2;  //忽略变量声明,GO里c的值从undefined变成2
    console.log(a);  //GO里a的值现在是1,打印结果为1
    function test(a){   //注意了!这里进入了函数体,我们在函数体中首先看的都是AO对象
        console.log(a);  //AO对象里a的值是function a() {},输出它
        var a=123;  //两部分,变量声明忽略,GO里a的值从function a() {}变成123
        console.log(a); //现在AO对象里a的值是123,输出它
        function a(){}  //确定AO对象已经看过了,忽略
        console.log(a); //AO对象里a的值仍是123,输出它
        console.log(b); //AO对象里b的值是undefined,输出它
        var b=function() {} //变量声明忽略,GO里b的值从undefined变成function() {}
        console.log(b);  //现在AO对象里b的值是function() {},输出它
        console.log(c);  //注意了!AO对象中没有c怎么办,那就从GO对象中找,输出2。分析结束。
     }
     test(2);   //进函数体里看看叭

总的来说,输出变量语句在函数体外面就从GO对象里找此变量,在函数体里优先从AO对象中找,找不到就去GO对象里找,GO对象也没有?那就会报错了。

作用域链

为什么找变量在函数体外面只能找GO对象,而在函数体里却优先找AO对象,找不到又可以去找GO对象呢?那这就有关变量作用域链了。

  • 什么是作用域([[scope]])——存储了运行期上下文的集合的容器
  • 什么是作用域链——作用域中所存储的执行期上下文对象的集合,这个集合呈链式链接,这种链式链接就是作用域链

函数定义时,会保留外层的作用域的变量,函数执行时,会创建执行期上下文,并且将执行期上下文放在作用域链的最顶端。

    var c=3;
    function a(){
        var a=1
        function b(){
            var b=2;
        }
    }
    var fun=a();

拿这个代码来说:函数a定义时,a的作用域链的0号位置会存放外层的GO对象,函数a执行时,会创建a的执行期上下文(以下称为aAO),并且放在最顶端,那么GO对象就往后移;函数b定义时,b的作用域链依次存放外层的aAO和GO,函数b执行时,创建b的执行期上下文(以下称为bAO),并且放在最顶端,其它对象就往后移。在函数a执行后的情况下作用域链如下图所示(红线需要被删去):

  • 为什么a的作用域链和aAO断开?因为函数a执行完了,a的执行上下文(aAO)被销毁;
  • 为什么b的作用域链和aAO断开(删去红线)?因为a的作用域链和aAO断开了,b保留a的作用域,自然也就和aAO断开了;

这幅图也可以解释为什么函数内部在AO中找不到所需变量时会去GO中找——查找变量从该函数作用域顶端依次向下查找,直到找到为止,而作用域链中GO在AO的后面,因此AO中找不到所需变量时自然会去GO中找。

来看个例子:

    function a() {
        function b() {
            function c() {
            }
            c();
        }
        b();
    }
    a();

我们将函数定义为defined,函数执行定义为doing,最后的变量作用域链是这个样子的:

    a defined a.[[scope]]-> 0:GO
    
    a doing   a.[[scope]]-> 0:aAO
                                         1:GO
                                         
    b defined b.[[scope]]-> 0:aAO
                                          1:GO
                                          
    b doing   b.[[scope]]->  0:bAO                                     
                                          1.aAO
                                          2.GO
                                          
    c defined c.[[scope]]-> 0:bAO                                      
                                         1.aAO
                                         2.GO
                                          
    c doing   c.[[scope]]->  0:cAO                                      
                                         1.bAO
                                         2.aAO
                                         3.GO

怎么样,是不是感觉其实不难,来写道题巩固巩固知识点叭

    function a() {
        var num=100;
        function b() {
            num++;
            console.log(num);
        }
        return b;
    }
    var demo=a();
    demo();
    demo();

答案是这个:

解析:即使a函数执行完毕后执行上下文(aAO对象)被销毁,但由于return b,b函数的scope中仍含有aAO,aAO中的num在第一次调用demo()时就已经增加1变成101,因此再次调用会在101的基础上加1变成102。

闭包

好耶!终于来到闭包了!

什么是闭包呢?当内部函数被保存到外部时,将会生成闭包。闭包会导致原有作用域链不释放,造成内存泄漏。

这位大神的评论更容易理解:

其实前面的题目在return b时就生成了闭包——函数a执行到最后一条语句:return b,此时b函数被返回到函数a的外部,并且继承了上级(函数a)作用域变量,接着函数a执行完最后一条语句,a的执行上下文被销毁。a的作用域链和aAO连接的那条线断开了,但由于b函数返回到函数a外部先于a的执行上下文被销毁,所以b的作用域链和aAO连接的那条线仍然存在。造成上级(函数a)作用域变量依然在占用内存,使内存减少,导致内存泄漏。

来道题检验一下你是否真正理解了上面这段话叭:

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

最后输出的是123

和上面的那段话一样,执行var demo=a();时a函数开始执行,执行到var aaa=123;时aAO中的aaa被赋值为123,return b;返回b函数后函数a执行完了最后一条语句,a的aAO被销毁,b的作用域链和aAO连接的那条线仍然存在,所以它的作用域链中还有aAO,输出aaa时,b先去找作用域链最顶层的bAO有没有这个变量,没有,那就继续去找aAO,找到了,输出123。

再来看道关于闭包的题叭:

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

答案和你预测的一样吗?

什么,你不会以为会从0一直输出到9吧?

如果和预测的结果有出入的话,那我们必须要解开两个问题:1.为什么输出结果是10?2.为什么输出了10个10?

  1. 先解决第一个问题:for循环的条件是i<10,也就是说当i==10时循环终止,所以最终i==10
  2. 再解决第二个问题:arr[i]等于一个函数体,直到调用它才会执行里面的语句,才会去找要输出的i是多少,在调用前i已经等于10了,所以最后也就只会输出10

所以这个过程就是:执行var myArr=test();语句时a函数开始执行,但是a函数执行时并不会执行function() {console.log(i);},因为它并没有被调用,a函数只会执行外层的for循环,执行完循环后i等于10,接着return arr;,执行完最后一条语句后a执行完毕,a与aAO之间的"线"断开,b与aAO之间的"线"继续存在。之后执行myArr[j]();,就是执行function() {console.log(i);},i等于10,就输出10,执行了十次,就输出了10次10。

补充知识点:立即执行函数

那要是我们就是想在有闭包的情况下最终打印出0到9怎么办?用立即执行函数就行了

啊立即执行函数又是个什么东西?很简单,就是字面意思——用function定义函数之后,会立即调用该函数,并且执行完立即被释放

这玩意要怎么用呢?

  • 比较建议的格式是:(function(形参){函数体} (实参))

例:

    var num=(function(a,b,c) {
        var d=a+b+c*2-2;
        return d;
      } (1,2,3));
  • 还有一种写法是:(function(){})()

要注意的是:(function(){})() 第二个()是执行符号,只有执行符号前面是表达式才能被执行。

至于为什么是这种写法,还有没有其它写法,这里没写,需要去看更多文章学习。

接下来我们就用立即执行函数做这道题叭:

    function test() {
        var arr=[];
        for (var i=0;i<10;i++) {
            //立即执行函数部分
            (function(j){
                arr[i]=function(){
                    console.log(j);
                }
            }(i))
        }
        return arr;
    }
    var myArr=test();
    for(var j=0;j<10;j++){
        myArr[j]();
    }

解释一下立即执行函数部分在其中起到什么作用:每次都将for循环中的i单独赋值给了立即执行函数里的i

        
        for (var i=0;i<10;i++) {
            (function(j){
                arr[i]=function(){
                    console.log(j);
                }
            }(0))//for循环中的 i 循环到0给立即执行函数里的 i 赋值0
            (function(j){
                arr[i]=function(){
                    console.log(j);
                }
            }(1))//for循环中的 i 循环到1给立即执行函数里的 i 赋值1
            
            ......
            
            (function(j){
                arr[i]=function(){
                    console.log(j);
                }
            }(9))//for循环中的 i 循环到9给立即执行函数里的 i 赋值9
        }

让我们看看最后的输出结果:

实现目标!

最后

到此,有关闭包的知识点就结束了。这篇文章我写得很啰嗦,因为想尽可能写得详细点,希望能够把我踩过的坑踏平。如果这篇文章能够帮到你就再好不过了!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值