Javascript学习---3、闭包(closure)
要成为高级 JavaScript 程序员,就必须理解闭包。当然,理解闭包必须先去理解执行环境和作用域(参考:我的前一篇文章Javascript学习---2、执行环境,作用域)
1、 作用域分配与变量访问规则
每个函数对象都有一个内部的 [[scope]]
属性,这个属性也由对象列表(链)组成。这个内部的[[scope]] 属性引用的就是创建它们的执行环境的作用域链,同时,当前执行环境的活动对象被添加到该对象列表的顶部。当我们在函数内部访问变量时,其实就是在作用域链上寻找变量的过程。对于此部分的内容如前所述,最好先去了解我的前一篇文章Javascript学习---2、执行环境,作用域)。
用代码再来说明一下,代码如下所示:
<script type="text/javascript"> function outer(){ var i = 10; function inner(){ var j = 100; alert(j);//100 alert(i);//10 alert(adf); } inner(); } outer(); </script> |
下图清晰的展现了上述代码的内存分配与作用域分配情况:
(摘自:笨蛋的座右铭)
下面来解释一下:(摘自:笨蛋的座右铭)
1.载入代码,创建全局执行环境,此时会在全局对象(window)中添加outer变量,其指向函数对象outer,此时作用域链中只有window对象.
2.执行代码,当程序执行到outer()时,会在全局对象中寻找outer变量,成功调用。
3.创建outer的执行环境,此时会新创建一个活动对象,添加变量i(赋上初始值undefined),添加变量inner,指向函数对象inner.并将活动对象压入作用域链中.并将函数对象outer的[[scope]]属性指向活动对象outer。此时作用域链为outer的活动对象+ 全局对象window.
4.执行代码,为 i 成功赋值。当程序执行到inner()时,会在函数对象outer的[[scope]]中寻找inner变量,找到后成功调用。
5.创建inner的执行环境,新建一个活动对象,添加变量j,并将该活动对象压入作用域链中,并函数对象inner的[[scope]]属性指向活动对象inner。此时作用域链为:inner的活动对象+outer的活动对象+全局对象window。
6.执行代码为j赋值,当访问i、j时成功在作用域中找到对应的值并输出,而当访问变量adf时,没有在作用域中寻找到,访问出错。
2、什么是闭包呢?
官方的说法太学术话了,根本不是人看的!其实,闭包就是嵌套在函数里面的内部函数,该内部函数使用了其外部函数中声明的局部变量、参数或其他内部函数。当该内部函数在外部函数外被调用时,就生成了闭包。(实际上任何函数都是全局作用域的内部函数,都能访问全局变量,所以都是window的闭包)。
另外,李松峰(《javascript高级程序设计第二版》译者)在《理解 JavaScript 闭包》中提出一个描述感觉不错,“闭包,就是封闭了外部函数作用域中变量的内部函数。但是,如果外部函数不返回这个内部函数,闭包的特性无法显现。如果外部函数返回这个内部函数,那么返回的内部函数就成了名副其实的闭包。此时,闭包封闭的外部变量就是自由变量,而由于该自由变量存在,外部函数即便返回,其占用的内存也得不到释放。”
如果用一句话来概括闭包,我觉得应该是“闭包就是能够读取其他函数内部变量的函数!”
通过示例来说明,如下代码所示:
在上述代码中,具有形成闭包的2个基本条件:第一,外层函数返回了一个内部函数;第二,该内部函数使用了外层函数中的局部变量。
照理,执行上述代码时,外层函数的局部变量在返回时就超出了作用域,因此increment()调用无法使用才对,即使能使用每次id都是从0开始的,也不应该输出1和2,这是为什么呢?这就是闭包的作用。应用上面讲到的作用域,我们可以得到如下所示的示意图:
根据该示意图,我们可以得知由于内层函数持有对外层函数的作用域,或者说内层函数引用了外层函数中的变量,这就使得在外层在执行完毕之后,Javascript的垃圾回收机制不会回收外层函数所占资源。(Javascript的垃圾回收机制:某个对象只有不再被引用,该对象才将被回收!)这就是闭包的作用!因此,在上述代码中,由于闭包的存在,使得外层函数中的变量i一直存在,所以,代码运行后,不断增加。
3、闭包的具体应用
Demo1:
<ul id="demo"> <li>Li 1</li> <li>Li 2</li> <li>Li 3</li> <li>Li 4</li> <li>Li 5</li> <li>Li 6</li> </ul> |
希望在上述Html代码结构中,每次单击li项时,弹出它的索引。代码如下:
var demo = document.getElementById('demo'); var lis = demo.getElementsByTagName('li'); for(var i = 0; i < lis.length; i++){ lis[i].onclick = function(){ alert(i ); //console.log(i); //console.log():ff控制台api,作用:在控制台打印出参数 } } |
上述代码在执行时,并不会产生我们想要的结果。实际上,每次单击时,都弹出的是“6”,而不是相应的i。为什么?这里就涉及到闭包和javascript的执行模型问题。这里onclick事件对应的匿名函数由于它使用了外部函数中的变量i,这就是一个典型的闭包了,该匿名函数的作用域中都保存着其外层函数的活动对象,所以每个click事件的匿名函数引用的是同一个i,而且变量i一直存在。同时,由于函数是在被执行的时候(而不是在定义的时候),会被推入一个栈环境中,才会创建自己的执行环境,在这个例子中,当用户点击的时候才会执行该函数,此时,程序已经循环完毕,i=6,所以每次都是6而不是想要的序号值。
如何解决上述问题,得到我们想要的结果呢。方法很多,具体可以参考《理解Javascript闭包》中提高的各种方法。这里只是列出其中的2个方法
方法1:添加一个匿名函数强制让闭包行为符合预期。代码如下所示:
var demo = document.getElementById('demo'); var lis = demo.getElementsByTagName('li'); for(var i = 0; i < lis.length; i++){ lis[i].onclick = function(num){ //注意:在原先的闭包外围包围了一个匿名函数 return function(){ alert(num); } }(i); //直接执行了匿名函数 } |
在上述代码中,没有直接把闭包赋值给数组,而是又定义了一个匿名函数,并将立即执行该匿名函数的结果赋值给了数组。该匿名函数有形参num,在调用每个匿名函数时,我们传入了变量i,由于函数参数是按值传递的,所以,就会将变量i的当前值复制给num,而在这个匿名函数内部,又创建并返回一个访问num的闭包。这样一来,resut数组中的每个函数都有自己num变量的一个副本,因此可以实现想要的结果了。
方法2:为遍历的每个元素添加自定义属性用来保存当前的索引值:
var demo = document.getElementById('demo'); var lis = demo.getElementsByTagName('li'); for(var i = 0; i < lis.length; i++){ lis[i].v = i; lis[i].onclick = function(){ alert(this.v); } } |
(注:这里还是想不明白为什么这样就行呢?)
参考资料:(学习时收集的资料)
1、http://www.cnblogs.com/fool/archive/2010/10/19/1855265.html
2、http://www.cn-cuckoo.com/2007/08/01/understand-javascript-closures-72.html#clFrmC
3、http://www.cnblogs.com/jeffwongishandsome/archive/2009/05/17/1458405.html
4、http://www.cnblogs.com/leo_wl/archive/2010/07/14/1777012.html
5、http://www.cnblogs.com/jxin/archive/2010/11/15/1877458.html
7、http://www.cnblogs.com/ljchow/archive/2010/07/06/1768749.html
8、http://jibbering.com/faq/notes/closures/
10、http://hax.javaeye.com/blog/273210
11、http://www.cnblogs.com/jenry/archive/2010/12/08/1900591.html
转载于:https://www.cnblogs.com/lzm525/archive/2010/12/12/1902707.html