• [转]真正搞清楚javascript闭包


    文章出处:http://www.w3cfuns.com/forum.php?mod=viewthread&tid=5593945&fromuid=5394455 

    分析的很深入,讲的也明白。。。

     

    还有一个高人阮一峰的,可以一起看。 (http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html)

     

    阮一峰对闭包的定义,我觉得很准:

    闭包就是能够读取其他函数内部变量的函数。

    由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"

    所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

     

      -----------------------------------------------------------------------------------------------------------------------------------------------------------------

         闭包这个东西,对js新手们来说确实不好理解,我自己在学习的时候,不夸张,看了不下20篇网上讲闭包的帖子,js的参考书也看了一堆,这里看懂点,哪里看懂点,最后综合起来,总算是搞清楚了。

        在我自学的过程中,我觉得网上的帖子要么太晦涩,要么没讲清楚,总之就是没有一篇特别适合新手们学习的帖子,我今天就结合各种实例仔细讲解(我把w3cfun 中之前 讲解闭包的精华帖中的例子都拿出来讲解,因为我觉得之前的帖子没有把实例讲解清楚,我当初看时也不懂为啥会是这样的运行结果),也当是自己复习梳理。 
         
    我相信,看完我的帖子,你再去看 为之漫笔 先生翻译的《理解javascript闭包》,就不会那么吃力了(我刚开始最少看了10遍也不懂)
                   
     献给新手,老鸟们请飘过!!  文章中可能会有用词不严谨的地方,但是保证新手们一定能看懂。

        ok
    ,废话不多说,进入正题。
        
    就想大家在n多讲解闭包帖子中看到的,要搞懂这个东西,作用域、执行环境(也叫运行上下文),作用域链的概念是必须先懂的,这个是前提,这个不懂,后面的就是在扯淡~~

        
    第一个实例  (这也是之前精华帖中的实例,请大家仔细看我下面的分析,会很长,但是会说明白原理)

       function outerFun()
       {
          var a =0;
          alert(a); 
       }
       var a=4;
       outerFun();     //0
     alert(a);          //4



         
    明白代码在执行前会预编译,然后再执行是关键。
         outerFun();     
         alert(a);
        
    上面这两行代码在运行之前,js的后台编译器会干下面的事情:

           
    编译器会先看看在全局代码中有没有 var关键字定义的变量和函数声明 我们这个例子中是有的,所以就在全局对象(也就是window对象)添加上 相应的属性,具体到我们的例子,现在的全局对象就是下面的样子(我觉得这样写大家肯定能看懂,我第一次看到都懂了)
      
    备注:( vo Variable Object  活动对象
        globalContext.VO
    (这个就是全局对象的意思) = {
              a: undefined 
            outerFun: <reference to 
    function  这里是对象的意思>
    };
            
    记住,在预编译阶段,所有的var关键字定义的变量,都被赋值:undefined 

        
    然后,把这个 globalContext.VO存入到函数 outerFun的内部属性中去,这个内部属性就叫做:[[scope]]

        
    预编译就结束了,就开始执行了。
         
    毫无疑问,执行的时候,会从上到下执行吧,所以
          var a=4  
    第一个被执行,发生标识符解析,因为这个代码是在全局环境中,所以就到全局对象中找下有没有a这个符号,
          
    发现globalContext.VO中有个叫 a的属性,在预编译的时候 globalContext.VO.a=undefined,所以 马上执行,             globalContext.VO.a变成了4.

        
    然后就轮到函数 outerFun 执行了,好,让人糊涂的事情又来啦。
         
    在执行outerFun() 时,js会为这段代码创建一个  执行环境(也叫执行上下文),然后函数outerFun中的代码就在这个执行环境中被执行。 这个执行环境在被创建时,会发生下面的事情:
          
    一个叫做 活动对象(简称:AO 家伙被建立了,这个家伙比较特殊,它拥有可访问的命名属性,但是它又不像正常对象那样具有原型(至少没有预定义的原型),而且不能通过 JavaScript 代码直接引用活动对象。
         
    这个活动对象会看看函数outerFun 里面有没有下面这3样东西:
        1  var
    关键字定义的变量
        2  
    函数声明
        3  
    函数形式参数
        
    我们的例子中,是有var关键字定义的变量的,所以它会给自己添加个属性a,然后同样赋值:undefined 
        
    可以这样理解:   AO.a=undefined.
         
    然后就会为函数outerFun 的执行环境分配作用域链,
         
    这个作用域链是这个样子的:outerFun.AO——outerFun[[scope]] 
         
    意思就是:outerFun函数的活动对象在最前面,然后就是outerFun函数在被定义时保持在它内部的[[scope]] 属性。
         
    这也是我们老在网上看到的,javascript权威指南中老说的一句话:”JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里.” 
    函数 outerFun [[scope]] 属性在预编译的时候就填入好了嘛,后面不管outerFun在哪里运行(调用),这个 [[scope]] 属性都不会变。

          
    ~~  准备工作终于全部完毕了,就开始正式执行代码内啦!
            
    首先执行这句  var a =0; 发生标识符解析

           js
    会首先在outerFun函数的活动对象中看看有没有a这个符号,如果没有,就到outerFun[[scope]] 中去找,outerFun[[scope]] 中又存入的是它定义时的 globalContext.VO
    所以在目前的情况下 这个标识符解析查找顺序就是  outerFun.AO——globalContext.VO

    很显然,a符号在outerFun.AO 就被找到了,所以a立刻被赋值为,变成这个样子:outerFun.AO .a=0;
           然后就执行alert(a)a标识符被解析,同样执行一遍查找:outerFun.AO——globalContext.VO
            outerFun.AO
    中找到a,值为0
             所以 alert(a)会弹出0.
            outerFun 函数就执行完了,然后执行 outerFun()后面的那句 alert(a)
            a标识符解析,因为句代码是定义在全局环境中的,同理,a符号只能在globalContext.VO中找吧,
           
    找到了,globalContext.VO.a=4.
           所以这个alert(a)就弹出4.

           ok 原理就是这样,下面来大量的看例子吧。

        function outerFun()
    {
    //
    没有var 
    a =0;
    alert(a); 
    }
    var a=4;
    outerFun();   //0
    alert(a);      //0


      同样,先预编译,globalContext.VO和上面的一摸一样。
      预编译完毕,执行var a=4,标识符解析,globalContext.VO中找到a,执行赋值,完毕后:globalContext.VO.a=4
      执行函数  outerFun(),创建执行环境,分配作用域链(同样还是outerFun.AO——globalContext.VO
      前面说了,函数  outerFun 的活动对象会在它自己内部查看3样东西:
    1  var关键字定义的变量
        2  
    函数声明
        3  
    函数形式参数

    这里都没有吧!!(a =0  这里的a没有用var定义,所以outerFun.AO中就没有a这个属性了)

        
    然后开始 执行代码:a =0;标识符解析:先在outerFun.AO中找,没找到,就跑到globalContext.VO中找,找到了,发现之前有个值是4 然后就修改globalContext.VO的值,变成这样:globalContext.VO.a=0;

       
    这里非常重要,解释了为啥在函数内部定义的变量不用var 关键字定义就是全局变量的意思,而且还会修改到全局变量中同样名字的属性值。

         
    然后执行代码:alert(a),标识符解析:outerFun.AO中没有,globalContext.VO中找到,值为
        
    所以弹出0

        outerFun函数执行完毕,执行下面的alert(a) 标识符解析:同理,代码定义在全局环境中,只能在globalContext.VO中找,找到 值为0 弹出

     

     -----------------------------------------------------------------------------------------------------------------------------------------------------------------

     代码变成下面的样子

        function outerFun()

    {

    //没有var

    a =0;

    alert(a);

    }

    /*var a=4;  请注意,我把这段代码注释掉了*/

    outerFun(); //0

    alert(a); //0

    结果还是都弹出0

    现在不论是全局还是函数内部都没有var定义的变量了,预编译阶段全局变量中不会有a这个属性,在outerFun执行环境创建时,outerFun.AO中也不会有这个叫a的属性。

      outerFun真正被执行的时候,发生a=0写入操作,按照顺序在outerFun.AO——globalContext.VO中都找不到a属性,由于是写操作,js就会自动的在globalContext.VO中添加一个a属性,并立刻写入值:0.

    所以后面不论是函数内部还是全局中执行 alerta),都会在globalContext.VO中查找到a属性了,就都弹出0.

    我再修改一下

    function outerFun()

    {

    //没有var

    a =0;

    alert(a);

    }

    alert(a); //错误,找不到a

    outerFun(); //0

         这里就会提示错误,由于先执行弹出(读)操作,我之前也说了, 在预编译阶段, globalContext.VO中是不会有a属性的(还记得么,预编译阶段globalContext.VO只看看有没有var定义的变量和函数声明),现在要直接执行a读操作,js还没来得及为globalContext.VO添加a属性(上面是先发生写入操作,globalContext.VO顺利添加属性a并赋值)所以就报错了。

       最后再简单讲2个类似的实例,我在坛子里看见有人贴的帖子有4js的面试题,我讲前2个,看懂了,后2个就能分析出来

    var a = 10;

    sayHi();   //20

    function sayHi() {

    var a = 20;

    alert(a);

    }

    alert(a); //10

     

            预编译完成,globalContext.VO.a=undefined

                              globalContext.VO.sayHi=sayHai 对象。

           从上往下执行代码,

              var a = 10;  执行完后,globalContext.VO.a=10

           轮到执行sayHi() 先创建执行环境,分配作用域链(sayHi内部有var定义的变量),所以 sayHi.Ao=undefined

           作用域链分配完毕:sayHi.Ao——globalContext.VO

            记住,正式执行sayHi函数之前,上面的操作就已经完成了。

            开始正式执行内部代码:  var a = 20;     发生标识符解析,按顺序查找 sayHi.Ao——globalContext.VO

            sayHi.Ao中找到a,执行写入操作,sayHi.Ao.a=20

             alert(a)  标识符解析 查找:sayHi.Ao——globalContext.VO,找到sayHi.Aoa=20

             弹出20

              sayHi()执行完毕

             执行全局中 alert(a);   只能在globalContext.VO中查找,找到a,弹出10  完毕

    2

    var a = 10;

    sayHi();

    function sayHi() {

    a = 20;

    alert(a);

    }

    alert(a);

                    预编译阶段一样

                     执行到sayHi,创建sayHi执行环境阶段, 没找到var sayHi.Ao中不会有a属性。

                     正式执行sayHia=20globalContext.VO标识符解析,查找:sayHi.Ao——globalContext.VO,在globalContext.VO中找到,将其值修改为20

                     alert(a);  标识符解析,查找:sayHi.Ao——globalContext.VO,在globalContext.VO中找到,弹出20.

                      sayHi执行完毕,

                   执行全局alert(a) 只能在globalContext.VO中查找,找到(已经被修改为20啦)

                   弹出20  全部代码完毕



    不要觉得这么简单的东西在这啰嗦这么多,我也没办法,程序执行时就是发生了这么多事情。 

     

     

    帖子太长了  写到回复里面

    Ok
    ,如果上面的都完全理解了,终于来看闭包吧,概念不写了,自己去看,先拿一个简单例子来讲,也是坛子里的,应该是管理员Alice转的帖子的里面的题目,没有讲解,估计新手不知道为啥(我刚开始就不知道)

    function say667() {
        var num = 666;
        var sayAlert = function() { alert(num); }
        num++;
        return sayAlert;



    var sayAlert = say667();
    sayAlert()  
    先预编译: globalContext.VOsay667say667函数对象。
    预编译完成,执行say667(),并将返回值赋予变量sayAlert
    say667建立执行环境,分配作用域链,活动对象被建立。
    其中 say667.Ao.num=undefind
    正式执行: var num = 666   //    say667.Ao.num=666
    var sayAlert = function() { alert(num); }  

    // 
    请注意,这里很重要 ,这里是一个函数表达式吧(不是函数声明,)所以到代码执行的时候才处理。为匿名函数分配作用域,并存入匿名函数的内部[[scope]]属性中,

    匿名函数的内部[[scope]]属性就成了这个样子:say667.Ao—globalContext.VO
    请注意这个匿名函数的内部[[scope]]属性,后面会用到

    然后匿名函数被存入变量sayAlert 中。
    num++        // say667.Ao.num=667  
    请注意,这里很重要,say667函数的活动对象中的num属性被变成667

    return sayAlert;  //   
    匿名函数function() { alert(num); } 被返回,

    那么 现在 全局变量sayAlert 就存入了这个function() { alert(num); } 
    最后执行sayAlert()  意思就是匿名函数function() { alert(num); } 被执行,就弹出这个num吧。
    问题的关键是 这个num去哪里找??
    请再回味javascript权威指南中这句话:”JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里.” 
    匿名函数function() { alert(num); } 最后被执行了,得为它分配执行环境,建立作用域链吧,创建活动对象?
    还记得我在帖子一中仔细提及的 一个函数的作用域链的样子吧,具体到这里就是:
    匿名函数.Ao—匿名函数内部[[scope]]属性

    去上面看看,匿名函数内部[[scope]]属性之前已经存好了吧?之前就定义好了吧??

    这也是为啥javascript权威指南要说:JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里.” 

    那么匿名函数的作用域链就成这样了:匿名函数.Ao——say667.Ao—globalContext.VO
    匿名函数内部就一句代码:alertnum),所以匿名函数自己的活动对象中没有num属性吧?
    现在num标识符开始解析,查找顺序:
    匿名函数.Ao——say667.Ao—globalContext.VO
    你看看num标识符能在上面哪个家伙的属性中找到?
    很显然找到say667.Ao中就有了吧 ,不就是667么?
    弹出667  搞定~~~~~~~~
    那我改动一下呢??

    function say667() {

    var num = 666;

    var sayAlert = function() { alert(num); }

    return sayAlert;

    num++;



    var sayAlert = say667();

    sayAlert() 
    num++
    还没来得及执行,函数sayAlert就执行完了,因为已经return sayAlert.ao.num=666
    sayAlert() //666
     

     

     

  • 相关阅读:
    有关TSQL的10个好习惯
    jQuery操作Select
    SQL Server 高性能写入的一些总结
    如何限制文本框只能输入数字,小数点,英文,汉字等各类代码(转载)
    补码、条件跳转指令
    Windows获取进程完整路径
    NumPy库
    WS_窗口风格常量
    C语言核心技术第一章
    Ubuntu安装搜狗输入法
  • 原文地址:https://www.cnblogs.com/suncms/p/2624121.html
Copyright © 2020-2023  润新知