学习Javascript闭包(Closure)

作者: 阮一峰

日期: 2009年8月30日

包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

下面就是我的学习笔记,对于Javascript初学者应该是很有用的。

一、变量的作用域

要理解闭包,首先必须理解Javascript特殊的变量作用域。

变量的作用域无非就是两种:全局变量和局部变量。

Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。

  var n=999;

  function f1(){
    alert(n);
  }

  f1(); // 999

另一方面,在函数外部自然无法读取函数内的局部变量。

  function f1(){
    var n=999;
  }

  alert(n); // error

这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!

  function f1(){
    n=999;
  }

  f1();

  alert(n); // 999

二、如何从外部读取局部变量?

出于种种原因,我们有时候需要得到函数内的局部变量。但是,前面已经说过了,正常情况下,这是办不到的,只有通过变通方法才能实现。

那就是在函数的内部,再定义一个函数。

  function f1(){

    var n=999;

    function f2(){
      alert(n); // 999
    }

  }

在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

  function f1(){

    var n=999;

    function f2(){
      alert(n); 
    }

    return f2;

  }

  var result=f1();

  result(); // 999

三、闭包的概念

上一节代码中的f2函数,就是闭包。

各种专业文献上的"闭包"(closure)定义非常抽象,很难看懂。我的理解是,闭包就是能够读取其他函数内部变量的函数。

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

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

四、闭包的用途

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

怎么来理解这句话呢?请看下面的代码。

  function f1(){

    var n=999;

    nAdd=function(){n+=1}

    function f2(){
      alert(n);
    }

    return f2;

  }

  var result=f1();

  result(); // 999

  nAdd();

  result(); // 1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

五、使用闭包的注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

六、思考题

如果你能理解下面两段代码的运行结果,应该就算理解闭包的运行机制了。

代码片段一。

  var name = "The Window";

  var object = {
    name : "My Object",

    getNameFunc : function(){
      return function(){
        return this.name;
      };

    }

  };

  alert(object.getNameFunc()());


代码片段二。

  var name = "The Window";

  var object = {
    name : "My Object",

    getNameFunc : function(){
      var that = this;
      return function(){
        return that.name;
      };

    }

  };

  alert(object.getNameFunc()());

(完)


==================================================

从javascript的循环问题来看待闭包本质

先看代码:demo.html

复制代码
 1 <!DOCTYPE HTML>
 2 <html>
 3 <head>
 4 <meta charset="utf-8"/>
 5 <title>闭包循环问题</title>
 6 <style type="text/css">
 7     p {background:red;}
 8 </style>
 9 </head> 
10 <body> 
11 <p id="p0">段落0</p> 
12 <p id="p1">段落1</p> 
13 <p id="p2">段落2</p> 
14 <p id="p3">段落3</p> 
15 <p id="p4">段落4</p> 
16 <script type="text/javascript" src="jquery-1.7.js"></script>
17 <script type="text/javascript"> 
18 ~function test() {   
19     for( var i=0; i<5; i++ ) { 
20          $("#p"+i).bind("click", function() {    
21              alert(i);    
22           });
23   };
24 }()
25 </script> 
26 </body> 
27 </html>
复制代码

每次循环就为对应的编号段落上添加一个click事件,事件的回调函数是执行一个alert();如果你以前没这么用过的话,估计也会认为单击某个段落就会弹出这个段落相应的编号0,1,2,3,4。但实际上是都是弹出5;

网上已经有很多讨论的博客了,他们给出了很多方法去实现弹出对应的编号。比较易于理解的方法如下:

1,将变量i保存在对应的段落的某个属性上:

复制代码
1 ~function test() {   
2     for( var i=0; i<5; i++ ) { 
3          $("#p"+i).bind("click", function() {
4              alert($(this).attr("index"));    
5           }).attr("index",i);
6   };
7 }();
复制代码

2,加一层闭包,i 以函数参数形式传递给内层函数:

复制代码
 1 ~function test() {   
 2     for( var i=0; i<5; i++ ) { 
 3          (function(param){
 4              $("#p"+i).bind("click", function() {
 5                 alert(param);  
 6           });
 7          })(i);
 8         
 9   };
10 }()
复制代码

当然还有其他一些方法,但是都不太好理解。

而我要探索的是,为什么demo.html中的返回值始终是5。网上的说法是“变量i是以指针或者变量地址方式保存在函数中”,因为只有按照这样理解,才能解释。可是仅仅凭借一个结论怎么才能服众了?

谈到指针或者变量地址这个话题,在C语言中倒是家常便饭了,但是在js这么性感的语言中,除了对象的及其对象属性的引用之外很少用到。一个基本的数据类型居然和指针拉上关系了,这勾起了探索的欲望。

3,试试下面的代码

复制代码
1 ~function test() {   
2     var temp =10;
3     for( var i=0; i<5; i++ ) { 
4         $("#p"+i).bind("click",function() {
5                 alert(temp);    
6          });
7     };
8     temp=20;
9 }();
复制代码
它的执行结果是每个段落的弹出都是20,而不是10。说明当我们在单击的那个时候,temp的值已经是20。这是个似乎不需要我来说明,很显然的结果,因为在我们单击之前,temp已经被赋值为20了。
4,再看看下面的代码,我们在temp被改变值之前有程序去触发单击事件,弹出的是10;
复制代码
 1 ~function test() {   
 2     var temp =10;
 3     for( var i=0; i<5; i++ ) { 
 4         $("#p"+i).bind("click",function() {
 5                 alert(temp);    
 6          });
 7          if(i===1){
 8           $("#p0").trigger("click");
 9          };
10     };
11     temp=20;
12 }();
复制代码

这说明我们在绑定$("#p0")的单击事件回调函数本来是要返回10的,当我再次手动去单击p0段落时,弹出20的原因是因为temp的值改变了。也就说明,每次弹出时,访问到的是temp此刻的值,而不是绑定时候的值;这可以说明在函数内部取得了变量的引用,而不是变量的值。扩展开去就是:函数内部访问了与函数同级的变量,那么该变量是常驻内存的访问该变量实质上是访问的是变量的地址;

通过以上的结论,那么我们可以简单的描述闭包的本质了:在子作用域中保存了一份在父级作用域取得的变量,这些变量不会随父级作用域的销毁而销毁,因为他们已经常驻内存了!

这句话也就说明了闭包的特性了:1:因为常驻内存所以会造成内存泄露  2,只要其他作用域能取到子作用域的访问接口,那么其他作用域就有方法访问该子作用域父级作用域的变量了。

看这样一典型的闭包的例子:

复制代码
 1 function A(){
 2    var a=1;
 3     
 4     function B(){
 5         return a++;
 6    };
 7    
 8     return  B;
 9 };
10 
11 
12 var  C=A();//C取得A的子作用域B的访问接口
13 console.log(C());//2    C能访问到B的父级作用域中的变量a   
复制代码

 

以上若有不足之处,欢迎指正,共同进步!


<!DOCTYPE HTML>
 <html>
  <head>
   <meta charset="gbk"/>
   <title>闭包循环问题</title>
   <style type="text/css">
     p {background:red;}
   </style>
 </head> 
 <body> 
  <p id="p0">段落0</p> 
  <p id="p1">段落1</p> 
  <p id="p2">段落2</p> 
  <p id="p3">段落3</p> 
  <p id="p4">段落4</p> 
<script type="text/javascript"> 
 for( var i=0; i<5; i++ ) {
    /*
    var obj = document.getElementById("p"+i);
    obj.setAttribute('qqq',i);
    obj.οnclick=function() { 
      alert( i +'==='+ this.getAttribute('qqq') ); //访问了父函数的变量i, 闭包 
    };
    */
    
    (function(i){
      var obj = document.getElementById("p"+i);
      obj.setAttribute('qqq',i);
      obj.οnclick=function() { 
        alert( i +'==='+ this.getAttribute('qqq') ); //访问了父函数的变量i, 闭包 
      };
    })(i);


 };
</script> 
</body> 
</html>

var fullname = 'John Doe';
var obj = {
   fullname: 'Colin Ihrig',
   prop: {
      fullname: 'Aurelio De Rosa',
      getFullname: function() {
         return this.fullname;
      }
   }
};

console.log(obj.prop.getFullname());
 
var test = obj.prop.getFullname;
 
console.log(test());




name = 'window';
var object1 = {
	name:'obj',
	getName: function(){
		var that = this;
		return function(){
			//alert(that.name);
			alert(that.name +'--'+ this.name);
		};
	}
}
object1.getName()();









  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值