[转]真正搞清楚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
 

 

 

原文地址:https://www.cnblogs.com/suncms/p/2624121.html