`

闭包原理详解——深入浏览器底层解析js的实际过程

 
阅读更多

 

一直没有看到过介绍得很详细的,大多其他文章都没涉及到活动对象如何形成的,它代表着什么,作用域链又是如何形成的,其结构是什么样子的,

所以这里自己写了一个。尽可能详尽,清晰,好理解。希望能帮到你。

 

一、先搞清概念[看官莫急,看完就有详细解析] 虽然刚开始会很难理解,但看完定会让你豁然开朗,顿时整个灵魂都感觉到升华了一般。 毕竟很多工作了5、6年的前端开发工程师都不一定会有时间精力潜心研究透这个概念的深层次原理。

先要了解js里变量和函数声明时所在的作用域,这是基础内容,不再啰嗦,请查查其他文档,这里举个例子,你可以照着例子来理解(有时简单理解一个概念也是为了更好的去理解其他依赖于它的更高级的概念), 

 

<script>
var a=1;
b=2;
//a 和 b 的作用域贯穿整个script,其实也是因为挂在了window上,用控制台打印window.a 就知道了。
function func1(){
       var a1 = 1;
       console.log(a); //因为a贯穿了整个,而func1也包含在整个里(window.func1也有值),所以这里也能访问到a
       function innerFunc1(){
            console.log(a1) //同理  a1的作用域贯穿了 func1,所以在这里面依然能够访问到
      }
}
</script>

 总之你可以把作用域看成一个变量在一个函数或者全局里的生命周期 或 覆盖的、影响的范围。

 

 

注意了,下面开始介绍闭包原理了:

javascript函数作用域分两个阶段 

                                一个叫创建时阶段 

                                一个叫运行时阶段 

定义1: 【看完例子以后不断对比着定义理解才能明白定义的意思】

所谓“创建时阶段”就是一个函数被以某种语法定义出来的时候, 

当此函数被创建在一个运行时阶段的作用域中时,此函数才为闭包状态,才能有自己的“创建时阶段”的作用域。[此话意思是函数的创建时阶段是在其所在的运行时阶段的作用域中的,简单说就是在上一级函数运行时才有下一级函数的创建时阶段; 并且一旦处于创建时阶段,这函数就处于闭包状态了]

 

定义2: 

所谓“运行时阶段”就是一个函数被()操作符标识为要运行的时候, 

(提示所有被()操作符标识为运行的函数都要在一个“运行时阶段”的作用域中才能被运行,如果不在则无法被运行) 

此时,函数本身会创建一个内部对象,叫“运行期上下文”对象, 

“运行期上下文对象”有自己的[[scope]]属性,此属性将copy此函数“创建时阶段”的[[scope]]属性的值,也就是“创建时阶段的作用域链”做为“运行时阶段的作用域链”的最初形态,

有了这个最初形态以后,运行期上下文将组织聚集函数内部的所有标识符等等属性为一个对象,这个对象叫做"可变对象"(有的文章书籍里叫活动对象),然后将这个可变对象的引用压入自己作用域链(运行时作用域)的第一个位置。  

 

[看到这里最好下载附件中的图解]

 

 

 

二、对定义的通俗阐述

 

 

<script type="text/javascript">
function a(){
     var i=0
     function b(){
          console.log(i++)  
     }
     return b;
}
var c=a();
c();
c();
</script>

 function a(){} 这是a函数的创建时阶段。 

 a();调用中(即a函数内部)是a函数的运行时阶段。

 

若a()是在全局位置被调用的(本例就是),那称为: 全局对象(window)创建了a()函数的运行时阶段。

 

伪代码【js执行时可以看成这样】:

function a(){
   /*
   还没调用a()时,浏览器js引擎先扫描一遍文档,扫到function a()...就创建一个a函数的声明(或表达为声明一个函数a),a此时就处于创建时阶段。此时a被创建在全局作用域的运行时阶段【全局作用域在script一开始就处于运行阶段了】,因此a()函数处于闭包(此时是全局作用域中的闭包)
   ,此时a函数有自己的 “创建时阶段”的作用域(设为 var creatTimeScope=[[globalRef]](用这个表示此时是对全局对象(全局活动对象)的引用。用数组表示这是个类似堆栈,等下可以在堆栈中压东西到栈顶(相当于插入到数组头部)[被压入的引用, [globalRef]]。globalRef表示全局作用域的引用,也是个堆栈。 以下涉及到数组的伪代码都要看成堆栈
   执行a()后,从此时开始,a函数就处于运行时阶段,运行时阶段对于a函数会做以下事情:    
    第1步:创建“运行期上下文”对象,暂时称之为 runTimeContextObj:相当于: 
     var runTimeContextObj={[[scope]]:null, creatActiveObj:function(){ //注意,这是伪代码。这个结构后面会用来表达运行阶段所做的事情。
         var activeObj = 组织聚集函数内部的所有标识符
         return activeObj
     }}     
     第2步:“创建时阶段的作用域链”做为“运行时阶段的作用域链”的最初形态(理解为初始化):  就是说runTimeContextObj.[[scope]]=creatTimeScope (既也 =[[globalRef]],回忆上面的等式 creatTimeScope=[[globalRef]]). 
     第3步:创建活动对象: activeObj=runTimeContextObj.creatActiveObj();组织聚集函数内部的所有标识符的意思就是把var变量声明 或者 function函数声明收集起来挂在活动对象上作为其属性来用的,你可以认为 activeObj有了i属性既 activeObj.i
     第4步:var activeObjRef=取activeObj地址
     第5步:把活动对象压入运行时上下文对象的[[scope]]栈顶: runTimeContextObj.[[scope]].push(activeObjRef) 。压入的是个活动对象的引用,那结果将是:
     runTimeContextObj.[[scope]][activeObjRef,[globalRef]] 即a的运行时对象中拥有本运行时的活动对象的引用和全局活动对象的引用, 注意[activeObjRef,[globalRef]]要看成个堆栈
     
     
    function b(){
        //同a()被全局对象调用的原理,b在a()运行时阶段(a的运行时作用域)里被创建,于是 var creatTimeScope=[[a的runTimeContextObj的引用]] 【关键,类比a获取全局对象的引用,是同样原理的,
       都是在上层函数运行时作用域里创建当前函数,使当前函数处于创建时阶段】
       
       同a的原理,当b被执行时: 
       b的上下文对象就成了runTimeContextObj.[[scope]][activeObjRef,[a的runTimeObj的引用]]
        而a的runTimeContextObj的引用即a上面的: [activeObjRef,[globalRef]],就是说最终是 
        b的runTimeContextObj.[[scope]][activeObjRef,[a的activeObjRef,[globalRef]]],把从b当前的活动对象的引用到全局的活动对象的引用都串起来了,
        b中找变量的时候找不到就会沿着这个顺序(activeObjRef,a的activeObjRef,globalRef)往上找,比如找那个i,现在自己的活动对象中找activeObj.i,找不到(因为没有声明),然后就找a的activeObj.i,找到了。
    }
    */
    return b;
}
var funcb= a(); //全局对象处调用a()
funcb();

 

 一般情况下,函数中声明的变量在运行完(运行时结束之后,可以理解为调用结束之后)后,变量就会被垃圾回收而销毁,

如 

function a(){
        var i=2
}
a();
console.log(i); //i在调用a()结束之后就销毁了

 但是上面的例子中function b处于闭包时,i却没有被垃圾回收了呢?我们知道js中对象只要在内存中还有引用就不会被销毁,顺着这个思路,我们考虑上面伪代码:

 var  funcb= a() 已经把b的创建时作用域creatTimeScope赋值给了 全局变量funcb,此时creatTimeScope不会被垃圾回收; 等到b被运行时 funcb(),照上面的分析creatTimeScope给了其运行时上下文对象 (b的runTimeContextObj),也就是等价于 funcb指向了b的runTimeContextObj了,这个runTimeContextObj一直存在有funcb引用,当然也就不会销毁,里面的活动对象也就存在,所以顺着 runTimeContextObj.[[scope]][activeObjRef,[a的activeObjRef,[globalRef]]]能访问到a的activeObjRef中的i。

所以闭包的本质是上下文对象的引用没有被垃圾回收。

 

顺带说一句,上面说的顺着上下文对象访问活动对象的属性或方法的过程就是常说的作用域链

 

三、常用场景

 

<ul id="ul1">
            <li id="li1">
                1
            </li>
            <li id="li2">
                2
            </li>
            <li id="li3">
                3
            </li>
            <li id="li4">
                4
            </li>
        </ul>
<script>
var ul1=document.getElementById('ul1'), ul1Lis=ul1.getElementsByTagName('li')
         // for(var i=0,len=ul1Lis.length;i<len;i++){
             // ul1Lis[i].addEventListener('click', function(){
                 // alert(i) //都是4
             // }, false);
         // }
         //改为闭包:
         // for(var i=0,len=ul1Lis.length;i<len;i++){
             // ul1Lis[i].addEventListener('click', (function(){
                 // var j=i //i是从这个外层函数执行时从for来的,且每次遍历都和下面的 return fun...共同放到一个闭包中,这个全局匿名函数的上一级是全局对象。用谷歌可以看作用域变量(Scope Variables)
                 // return function(){
                     // alert(j) //是每次循环变量中的i,这i已经被放在外层的那个匿名函数的闭包中了. 用两重闭包是因为addEventListener的第二个参数需要一个函数作为监听器
                 // }
             // })(), false);
         // }
         
         function closureA(){
             for(var i=0,len=ul1Lis.length;i<len;i++){
                 ul1Lis[i].addEventListener('click', (function(){
                     var j=i //对比上面那段,多了一级闭包的活动对象。
                     return function(){
                         alert(j) 
                     }
                 })(), false);
             }
         }
         closureA()
</script>

 

 

 //特殊问题-------------------:

        

//函数内部没有var的语句成为了全局变量-------------------:
         function a(){
             var1=1
             function b(){                
                console.log(window.var1) //1
                window.var1=2 //为了看下句引用的 var1 是从全局活动对象来的,还是从a的活动对象来的。
                console.log(var1) //2 这样看来这是从全局活动对象来的
             }
             return b;
         }
         a()()
         /**
          * a中的 var1没有var,这将会在a运行时的组织活动对象过程时不会把var1设置到a活动对象上(因为没有var,可见组织标识符的过程是看有没有var【function应该也类似】)
          * 当函数执行到此句时,js开始进入寻找变量的过程,沿着作用域链往上找,一直找到顶都没找到,那么就会在顶部创建一个这样名称的变量。相当于在开始时全局声明的var,只不过这时是声明和运行同时进行了。
          * 
          */
         
         //with虽然插入了外部作用域的活动对象,但却不像函数那样把里面的var组织成活动对象-------------------:
         function a_with(){
             var var2={
                 p1:1,
                 p2:2
             }
             function b(){
                 var var3=3
                 with(var2){ //当前函数的作用域链的前端又插入了一个对象:var2
                     var var4=4 //在b函数运行阶段,分析函数内的代码,组织变量时也会把这个放到b的活动对象中。而不是在with的作用域链中,难道with不创建活动对象,仅仅是把一个对象插入作用域链前面?
                     console.log(var2.p1)
                     console.log(var2.p2)
                     console.log(var3) //访问自己函数内的变量也是跨了一个作用域哦。
                     console.log(var4)
                 }
                 console.log(var4) //4
             }
             return b;
         }
         a_with()()

 

 

 

 

分享到:
评论

相关推荐

    python闭包深入(csdn)————程序.pdf

    python闭包深入(csdn)————程序

    深入理解javascript原型和闭包

    深入理解javascript原型和闭包(01)——一切都是对象 深入理解javascript原型和闭包(02)——函数和对象的关系

    javascript闭包详解中文word版

    资源名称:javascript闭包详解 中文word版   内容简介: Javascript中有几个非常重要的语言特性——对象、原型继承、闭包。其中闭包 对于那些使用传统静态语言C/C 的程序员来说是一个新的...

    尚硅谷——JavaScript闭包

    JavaScript闭包 JavaScript闭包 JavaScript闭包 JavaScript闭包

    JavaScript解析机制与闭包原理实例详解

    本文实例讲述了JavaScript解析机制与闭包原理。分享给大家供大家参考,具体如下: js解析机制: js代码解析之前会创建一个如下的词法环境对象(仓库):LexicalEnvironment{ } 在扫描js代码时会把: 1、用声明的方式...

    javascript闭包详解

    javascript闭包详解 javascript闭包详解 javascript闭包详解

    Python闭包:深入理解与应用场景解析.zip

    python通过本文的介绍,你应该对Python中的闭包有了更深入的理解,并能够开始探索如何在你的项目中应用这一特性。掌握闭包的使用,可以帮助你编写出更加强大和灵活的程序。

    Javascript 闭包完整解释

    Javascript 闭包完整解释

    JavaScript闭包函数

    闭包是ECMAScript (JavaScript)最强大的特性之一,但用好闭包的前提是必须理解闭包。闭包的创建相对容易,人们甚至会在...而闭包工作机制的实现很大程度上有赖于标识符(或者说对象属性)解析过程中作用域的角色。

    闭包概念原理

    闭包(closure)。闭包其实大家都已经谈烂了。尽管如此,这里还是要试着从理论角度来讨论下闭包,看看ECMAScript中的闭包内部究竟是如何工作的

    JAVASCRIPT闭包详解

    而闭包工作机制的实现很大程度上有赖于标识符(或者说对象属性)解析过程中作用域的角色。 关于闭包,最 简单的描述就是 ECMAScript 允许使用内部函数--即函数定义和函数表达式位于另一个函数的函数体内

    深入理解JavaScript系列

    深入理解JavaScript系列(3):全面解析Module模式 深入理解JavaScript系列(4):立即调用的函数表达式 深入理解JavaScript系列(5):强大的原型和原型链 深入理解JavaScript系列(6):S.O.L.I.D五大原则之...

    python 闭包和装饰器(csdn)————程序.pdf

    python 闭包和装饰器(csdn)————程序

    javascript深入理解js闭包.docx

    javascript深入理解js闭包.docx

    理解_JavaScript_闭包

    本文结合 ECMA 262 规范详解了闭包的内部工作机制,让 JavaScript 编程人员对闭包的理解从“嵌套的函数”深入到“标识符解析、执行环境和作用域链”等等 JavaScript 对象背后的运行机制当中,真正领会到闭包的实质。

    JavaScript闭包

    Javascript中有几个非常重要的语言特性——对象、原型继承、闭包。其中闭包 对于那些使用传统静态语言C/C++的程序员来说是一个新的语言特性。本文将以例子入手来介绍Javascript闭包的语言特性,并结合一点 ...

    【JavaScript源代码】js闭包和垃圾回收机制示例详解.docx

    js闭包和垃圾回收机制示例详解  目录 前言 正文  1.闭包  1.1闭包是什么?  1.2闭包的特性 1.3理解闭包  1.4闭包的主要实现形式  1.5闭包的优缺点  1.6闭包的使用  2.垃圾回收机制 总结  前言  正文  ...

    JS闭包可被利用的常见场景

    JS闭包可被利用的常见场景。值得保留的文档。值得一看

Global site tag (gtag.js) - Google Analytics