锋利的JQ看完,也不能说完全看完吧。至少介绍如何写JQ得插件还没有怎么仔细看。只因为有一个词一直勾引着我。是的,这本书介绍不多的“闭包”。
本着学习的精神,开始翻书谷歌度娘查找关于javascript闭包的概念以及资料。
在《javascript高级程序设计》(第三版)的第七章函数表达式中有一行简单的介绍:闭包是指有权访问另一个函数作用域中的变量的函数。如何去理解这二十个字不到的解释?本文就采用一种按图索骥的方式尝试着写下去。
函数与作用域
先说函数的吧。
首先,函数是个对象。这对于我以前写java的孩子来说是觉得不可思议的事情。但事实就是如此。实验如下:
true
Object instanceof Function
true
关于第二句我想不少新手第一次看到这个会跟我同样惊异了,感情这两个货既当儿子又当爸爸啊?后来一番查证确实如此,这个放到后边去总结。但这确实是可以证明了函数是对象。常用函数的形式不外乎如下几种:
function foo() {//或者
...
}
(function () {
...
})();
不过这两种形式却代表了函数声明与函数表达式。两者区别是很大的。这涉及到变量对象(Variable Object)、上下文阶段、执行代码阶段等概念。后续会慢慢道来。在《javascript高级程序设计》提到,解析器在向执行环境中加载数据是,对函数声明和函数表达式并非一视同仁。解析器会率先读取函数声明,并使其在代码执行任何代码之前可用(可访问);至于函数表达式,则必须等解析器执行到它所在的代码行才会真正被执行。这也是是javascript的Hoisting机制(变量提升)一种体现吧。关于两者的区别,我参考了园子里汤姆老师的博文:
http://www.cnblogs.com/TomXu/archive/2012/01/30/2326372.html
http://www.cnblogs.com/TomXu/archive/2011/12/29/2290308.html
针对上面汤姆老师的上面一篇文章有个例子是这样的:
var foo = {}; (function initialize() { var x = 10; foo.bar = function () { alert(x); }; })(); foo.bar(); // 10; alert(x); // "x" 未定义
汤姆老师对此的描述是:“我们看到函数foo.bar(通过[[Scope]]属性)访问到函数initialize的内部变量“x”。同时,“x”在外部不能直接访问。在许多库中,这种策略常用来创建”私有”数据和隐藏辅助实体。”我想做出一些补充,为什么我们调用不到initialize(),结果为“initialize is not defined”。在进入全局上下文的时候,由于initialize不是函数声明而是函数表达式,因此initialize不会存在variable object中,自然,在进入代码执行阶段时,variable object中还是不会有initialize()。未保存的函数表达式只有在它自己的定义或递归中才能被调用(这是汤姆大叔原话)。那么foo.bar属性是在什么时候建立的呢?在函数表达式initialize执行之前,进入上下文阶段的时候,foo.bar属性被创建,并在initialize执行时被赋值。可以做个小实验验证一下的。我们把foo.bar();的位置调整到var foo={};的下面,此时initialize还没有执行,当然foo.bar也还没有创建以及赋值,打印结果为:TypeError: Object #<Object> has no method 'bar'。
说到函数,不得不提及arguments。在《javascript高级程序设计》一书中3.7.1节有这样一段介绍:ECMAscript函数不介意传递进来多少个参数,也不在乎传进来的参数是什么类型。在犀牛书《javascript权威指南》中还介绍到arguments允许完全地存取那些实际参数值,即使某些参数还没有命名,即函数体内通过arguments对象来访问参数数组。但要说明的是arguments并不是一个数组。这个再犀牛书和高级程序设计两本书都提到。关于一些函数的细节问题,回头继续看看两本书,相互印证一下。
关于作用域。
在js高级程序设计一书中讲得比较简明易懂。搬运书中代码:
var color="blue"; function changeColor(){ var anotherColor="red"; function swapColors(){
var tempColor=anotherColor; anotherColor=color; color=tempColor;
alert(tempColor+" "+color+" "+anotherColor); // swapColor()局部环境,这里可以访问tempColor,color,anotherColor } //changeColor()局部环境,这里可以访问color,anotherColor swapColors(); } //全局环境,这里可以访问color changeColor();
// red blue red
上述共涉及3个执行环境:全局环境,changeColor()局部环境和swapColor()局部环境。对应如上段代码中。形成的作用域链可以这样表示:
window
├── color
└── changeColor()
├── anotherColor
└── swapColor()
└── tempColor
这样的作用域链看起来就非常直观了。在犀牛书4.7节中有这样一句描述:每个javascript执行环境都有一个和它关联在一起的的作用域链。这个作用域链是一个对象列表或对项链。我觉得汤姆大叔对这句话的解释就非常直白易懂:
Scope = AO|VO + [[Scope]]
// 函数作用域链=函数活动对象+父级函数作用域链
这种递归的方式有木有眼前一亮的感觉!递归的方式也就是一个链条一样。把scope都链起来了。
回到刚才的例子,让我们就上面的例子稍作改动:
var color="blue"; function changeColor(){ var anotherColor="red"; function swapColors(){ var tempColor=anotherColor; anotherColor=color; color=tempColor; alert(color+" "+anotherColor +" "+tempColor); } return swapColors;
}
var gColor=changeColor();
gColofr();
// red blue red
改动如下:我们让changeColor函数返回 swapColors();在全局区用一个gColor变量接收changeColor()的返回值。调用gColor(),我们居然在全局环境访问到局部环境的参数了?!是的,从打印结果来看,我们是访问到了。尝试着分析一下过程吧:当代码执行到var gColor=changeColor();这一步时,会进入changeColor函数的“上下文”中,创建活动变量(activation object),属性有anotherColor和swapColors。在函数changeColor被创建时[[scope]]属性也被创建其中。对于changeColor来说:
changeColor.[[Scope]] = [Global];
对于swapColor函数来说:
swapColor.[[Scope]] = [AO+changeColor.[[scope]]];
此时,changeColor函数的返回值被变量gColor引用,由于swapColor函数通过作用域链分别可以找到tempColor、anotherColor以及color属性。(在本作用域中找不到的属性去上一层中都找到了)。这样内部函数(swapColor)可以引用外部函数(changeColor)的参数以及变量,形成了一直勾引我的“闭包”效果。
自此,终于讲到点子上了。同时,我们再关注一下上面的例子,gColor是一个全局变量(毫无疑问),作为全局变量,它可以在全局环境中随时被调用,那么GC就不会回收它。那么被gColor引用的anotherColor,tempColor会被回收么?答案是显而易见的了。综上,闭包也就有了这两个功能,那就是网上各种资料所说的:一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
等等,还没完。
嘿嘿,还没完,我可还没有忘记上面提到的两个概念:上下文阶段,执行代码阶段。
关于这两个概念,我找到一篇比较好的文章来详细拜读,对于我这个JS小菜鸟来说吸收得不错,不敢藏着掖着,请看360weboy的这篇文章:
http://www.360weboy.com/frontdev/javascript/execution-context.html。
在此搬运其中一段:找到当前上下文中的调用函数的代码在执行被调用的函数体中的代码以前,开始创建执行上下文
进入第一个阶段-建立阶段:建立variableObject对象:
-
建立arguments对象,检查当前上下文中的参数,建立该对象下的属性以及属性值。
- 检查当前上下文中的函数声明,每找到一个函数声明,就在variableObject下面用函数名建立一个属性,属性值就是指向该函数在内存中的地址的一个引用如果上述函数名已经存在于variableObject下,那么对应的属性值会被新的引用所覆盖。
- 检查当前上下文中的变量声明:每找到一个变量的声明,就在variable Object下,用变量名建立一个属性,属性值为undefined。如果该变量名已经存在于variableObject属性中,直接跳过(防止指向函数的属性的值被变量属性覆盖为undefined),原属性值不会被修改。
- 初始化作用域链。
-
确定上下文中this的指向对象。
此后进入第二个阶段-执行阶段:
执行函数体中的代码,一行一行地运行代码,给variableObject中的变量属性赋值。
再等一下,还有点!
还记得前面那个即做老爹又做儿子的事情么?这里不得不引出javascript基础の概念:原型链。
在此,有句题外话。原型对象本质是也是一个对象实例。比如说函数changeColor.prototype 就是一个原型对象.其__proto__属性===Object.prototype。
关于这一部分的概念,请看360weboy的这篇文章,我是在是无法再拿出更浅显的语言来说明了:
http://www.360weboy.com/frontdev/javascript/javascript_prototyp.html
结合这张神图:
引用汤姆大叔博文里的某位网友的留言相信会记得更牢了:
Object 是对象的祖先
Function 是函数的祖先
函数可以做构造器
对象是函数new出来的
构造器的prototype是对象
对象的__proto__指向构造器的prototype。
最后
好累,呵呵,不过终于把javascript的一些基本概念给弄完了,肯定有不少概念需要继续强化巩固的。如果哪里有错误,请各位不吝指教。
接下来的时间准备一下答辩的事情,顺便把犀牛书和javascript高级程序设计这两书好好研读。
附上几位老师的地址:
http://www.cnblogs.com/TomXu/ 汤姆大叔
http://www.cnblogs.com/CaiAbin/category/262670.html 菜阿彬
http://www.360weboy.com/category/frontdev/javascript 360weboy
感谢诸位的知识总结。小生再次谢过。