您的位置:首页 > Web前端 > JavaScript

[转]真正搞清楚javascript闭包

2012-08-05 19:30 232 查看
文章出处: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立刻被赋值为0 ,变成这个样子: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
所以弹出0

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

所以后面不论是函数内部还是全局中执行 alert(a),都会在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个类似的实例,我在坛子里看见有人贴的帖子有4道js的面试题,我讲前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.Ao中a=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属性。

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

alert(a); 标识符解析,查找:sayHi.Ao——globalContext.VO,在globalContext.VO中找到a ,弹出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.VO.say667=say667函数对象。
预编译完成,执行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
匿名函数内部就一句代码:alert(num),所以匿名函数自己的活动对象中没有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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: