学习Javascript闭包(Closure)

时间:2022-05-23 22:40:01

作者: 阮一峰

日期: 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

学习Javascript闭包(Closure)
 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>
学习Javascript闭包(Closure)

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

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

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

学习Javascript闭包(Closure)
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 }();
学习Javascript闭包(Closure)

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

学习Javascript闭包(Closure)
 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 }()
学习Javascript闭包(Closure)

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

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

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

3,试试下面的代码

学习Javascript闭包(Closure)
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 }();
学习Javascript闭包(Closure)
它的执行结果是每个段落的弹出都是20,而不是10。说明当我们在单击的那个时候,temp的值已经是20。这是个似乎不需要我来说明,很显然的结果,因为在我们单击之前,temp已经被赋值为20了。
4,再看看下面的代码,我们在temp被改变值之前有程序去触发单击事件,弹出的是10;
学习Javascript闭包(Closure)
 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 }();
学习Javascript闭包(Closure)

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

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

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

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

学习Javascript闭包(Closure)
 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
学习Javascript闭包(Closure)

 

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


<!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.onclick=function() {
alert( i +'==='+ this.getAttribute('qqq') ); //访问了父函数的变量i, 闭包
};
*/

(function(i){
var obj = document.getElementById("p"+i);
obj.setAttribute('qqq',i);
obj.onclick=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()();