最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 「JavaScript深入」理解闭包

    正文概述 掘金(SwordQiu)   2021-02-03   480

    在《你不知道的Javascript》中,有一道题

    for(var i=0; i < 6; i++) { 
        setTimeout(function(){ 
            console.log(i); 
        },0); 
    }
    

    答案是6个6。

    这道题粗略的解释是i属于全局作用域,在异步定时器里,也引用的是相同作用域中的i,当定时器启动时,i已经变成了6,所以打印出来的结果并不是我们预期的0、1、2、3、4、5。

    那么有没有更加专业(zhuangbility)一点的说法呢?有的,我们可以从执行上下文和闭包的角度来解题。

    今天我们来深入说一说其中的原理

    闭包是什么

    首先我们需要知道闭包究竟是什么东西。

    MDN中对于闭包的解释是这样的:

    这么专业的解释肯定看不懂啦,我们来看看它给的例子:

    function init() {
        var name = "Mozilla"; // name 是一个被 init 创建的局部变量
        function displayName() { // displayName() 是内部函数,一个闭包
            alert(name); // 使用了父函数中声明的变量
        }
        displayName();
    }
    init();
    

    以上代码非常简单,它的大概意思就是displayName函数里并没有name这个变量,它引用了外层作用域中的name,那么displayName就是一个闭包

    所以我们用大白话定义一下:闭包 = 函数 + 访问外层作用域的变量。

    执行上下文与闭包的关系

    要明白执行上下文与闭包的关系,首先我们需要写一下上面例子中的执行上下文过程:

    1、创建全局上下文,压栈

    ECStack=[globalContext]
    

    2、全局上下文初始化完成

    globalContext={
       VO:{  //全局变量对象中有个init函数变量声明
          init:referance to function init
       },
       Scope:[globalContext.VO],
       this:globalContext.VO
    }
    

    同时init函数内部有个[[scope]]属性保存了globalContext.VO

    3、调用函数,创建函数上下文后压栈

    ECStack=[globalContext,initContext]
    

    4、进入上下文,此时函数并没执行,初始化函数上下文,将[[scope]]内部属性复制给函数上下文中的Scope属性,并将当前上下文中的AO(活动变量对象)放到最前面

    initContext={
       AO:{
          arguments:{
             length:0
          },
          name:undefined,
          displayName:referance to function displayName
       },
       Scope:[AO,globalContext.VO],
       this:undefined
    }
    

    5、函数代码执行,变量对象完成赋值

    initContext={
       AO:{
          arguments:{
             length:0
          },
          name:Mozilla,
          displayName:referance to function displayName
       },
       Scope:[AO,globalContext.VO],
       this:undefined
    }
    

    6、遇到displayName代码,此时内部生成[[scope]]属性保存外层作用域层级链

    displayName.[[scope]]=[initContext.AO,globalContext.VO]
    

    7、开始调用displayName函数,创建函数上下文,压栈

    ECStack=[globalContext,initContext,displayNameContext]
    

    8、进入函数上下文,初始化displayNameContext,把活动对象压入作用域链中

    displayNameContext={
       AO:{
          arguments:{
             length:0
          },
       }
       Scope:[AO,initContext.AO,globalContext.VO]
    }
    

    9、函数执行,完成变量对象赋值,然后找到外层的name,打印

    10、开始弹栈

    // 先弹displayName的上下文
    ECStack=[globalContext,initContext]
    
    // 再弹init的上下文
    ECStack=[globalContext]
    

    以上就是执行上下文的过程,而displayName之所以能够访问到外层作用域中的name,就是因为displayName中的Scope属性,里面保存了initContext和全局上下文中的变量对象。

    displayName.Scope=[AO,initContext.AO,globalContext.VO]
    

    有了这个属性,即使initContext被弹出执行栈了,displayName同样可以获取到它的变量对象。

    这就是闭包的底层逻辑。同时也是闭包和执行上下文的关系

    回到开始

    现在我们来重新审视一下最开始的代码

    for(var i=0; i < 6; i++) { 
        setTimeout(function(){ 
            console.log(i); 
        },0); 
    }
    

    当执行function函数时,此时的执行上下文有什么呢?全局上下文变量对象中有一个i

    globalContext.VO={
       i:6
    }
    

    我们可以修改一下代码使它变成我们想要的0、1、2、3、4、5

    1、第一种改法

    for(var i=0; i < 6; i++) { 
        setTimeout(function(i){ 
            console.log(i); 
        },0,i); 
    }
    

    使用这种方法,会让setTimeout函数传ifunction,此时匿名函数第一次执行时它的上下文中是这样的

    functionContext={
       AO:{
          arguments:{
             0:0,  // 实参中的 i
             length:1
          },
          i:0,
       }
      ...
    }
    

    函数调用时,i已经被定时器当成参数传递给匿名函数了。第一次匿名函数执行时i为0。

    2、第二种改法

    for (var i = 0; i < 6; i++) {
      (function (i) {
        setTimeout(() => {
          console.log(i);
        }, 0);
      })(i);
    }
    

    类似第一种改法,只是此时i保存在外层立即执行函数的变量对象里面

    外层匿名函数Context = {
        AO: {
            arguments: {
                0: 0,
                length: 1
            },
            i: 0
        }
    }
    

    内层匿名函数的Scope属性为

    [AO,外层匿名函数Context.AO,globalContext.VO]
    

    内层匿名函数会顺着作用域链查找i,找到外层匿名函数Context.AO,取到i的值

    3、第三种改法

    for(let i=0; i < 6; i++) { 
        setTimeout(function(){ 
            console.log(i); 
        },0); 
    }
    

    使用let声明不会将i挂在全局变量对象下(跟var声明不是同一个)。此时开辟了不同的变量对象。

    定时器参数函数Context={
       AO:{
          length:0
       },
       Scope:[AO,无名氏作用域变量对象,globalContext.VO]
    }
    

    于是定时器参数函数function顺着Scope属性找到对应的i


    起源地下载网 » 「JavaScript深入」理解闭包

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元