闭包两三事

本文详细解释了JavaScript闭包的概念,包括变量作用域、如何从外部读取局部变量、闭包的用途及注意事项,通过实例代码帮助理解闭包的运行机制。
闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。
下面就是我的学习笔记,对于Javascript初学者应该是很有用的。

一、变量的作用域

要理解闭包,首先必须理解Javascript特殊的变量作用域。
变量的作用域无非就是两种:全局变量和局部变量。

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

	var n=999;//函数外边用var声明的变量为全局变量
	//函数的声明
  function f1(){
    alert(n);//显示全局变量
  }
  f1(); // 函数的调用:结果为:999

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

	function f1(){
    var n=999;//函数内部用var声明的变量为局部变量
  }
  alert(n); //访问函数内部的局部变量会报错:JavaScript error: Uncaught ReferenceError: n is not defined

这里有一个地方需要注意,函数内部声明变量的时候,一定要使用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;//f1中定义的局部变量
    function f2(){
      alert(n); //访问f1中定义的局部变量n
    }
    return f2;//将f2返回
  }
  var result=f1();//f1执行后将f2()返回赋给result,但是此时f2()并未执行
  result(); //结果为:999

三、闭包的概念

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

各种专业文献上的"闭包"(closure)定义非常抽象,很难看懂。

我的理解是,闭包就是能够读取其他函数内部变量的函数。

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

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

四、闭包的用途

闭包可以用在许多地方。它的最大用处有两个:

一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

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

	function f1(){
    var n=999;//f1()中声明的一个局部变量并赋值
    nAdd=function(){n+=1}//将匿名函数赋值给全局变量nAdd
    function f2(){
      alert(n);//访问f1()中声明的局部变量n
    }
    return f2;
  }
  var result=f1();//执行f1之后,返回值为f2(),因此result实质上是对f2()的一个引用,但是f2()此处还未执行
  result();//执行f2(),结果为:999
  nAdd();//由于执行f1()的时候,nAdd=function(){n+=1}只类似于一个赋值操作,故并未执行。此处执行结果为:1000
  result();//再次执行f2(),因为上一行中nAdd()已经对局部变量n进行加1操作,故结果为: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";//声明一个命名为name的全局变量,并赋值为The Window
  var object = {
    name : "My Object",//object对象包含一个名为name的属性,其值为:My Object
    getNameFunc : function(){
      return function(){
        return this.name;//this代表的是当前执行代码的环境对象
      };
    }
  };
  alert(object.getNameFunc()());//结果为:The Window

解析:

首先object.getNameFunc()函数执行后返回一个匿名函数,此时匿名函数中的this指代的是当前的对象window,因此匿
名函数中this.name为全局变量name,即结果为:The Window。

代码片段二。

	var name = "The Window";//声明一个命名为name的全局变量,并赋值为The Window
  var object = {
    name : "My Object",//object对象包含一个名为name的属性,其值为:My Object
    getNameFunc : function(){
      var that = this;//此处的this指代的是当前对象object,故that也为object,保存this的值,避免指代转移
      return function(){
        return that.name;//that代表object
      };
    }
  };
  alert(object.getNameFunc()());//结果为:My Object

解析:

当执行完object.getNameFunc()函数后,返回一个匿名函数,由于在getNameFunc中将this的值(object)保存于that
中,因此匿名函数中的that.name便为object.name,故结果为:My Object。

代码片段三。

	function fun(n,x){
		document.write("<br>x="+x+"<br>");//在页面上打印fun()的第二个参数的值,若未传则为undefined
		return {
			fun:function(m){
				return fun(m,n);
			}	
		};
	}
	var a = fun(0);//undefined
	a.fun(1);//0
	a.fun(2);//0
	a.fun(3);//0
	
	var b = fun(0).fun(1).fun(2).fun(3);//undefined/0/1/2
	
	var c = fun(0).fun(1);//undefined/0
	c.fun(2);//1
	c.fun(3);//1

解析:

var a = fun(0);----在开始执行fun(0)函数时,实参0传给第一个参数n,第二个参数x未传值,故x为:undefined。
				   当fun()函数执行完成后,将包含fun方法的对象返回给全局变量a,故此处a为一个对象。【因为
				   返回对象a的fun(m,n)方法中用到其外层函数fun()中的局部变量n(函数中的参数也是函数中的局
				   部变量),因此在fun()函数调用结束之后局部变量n不会被立即销毁,故n一直保存于内存中,其
				   值为在执行fun(0)时传的参数0。】
a.fun(1)-----------当执行a.fun(1)方法时,实参1传给m,返回值为函数fun(m,n),因为n=0,故执行fun(m,n)即等
				   价于fun(1,0),因此打印结果为:x=0。【因为a.fun(1)的返回值为函数fun(1,0),故当fun(1,0)
				   执行结束之后会返回另一个新的对象,此新对象的参数n的值为此处传参1,因此此次执行返回函数
				   fun(1,0)时并不影响a对象中fun()方法中n的值。】
a.fun(2)-----------当执行a.fun(2)方法时,实参2传给m,返回值为函数fun(m,n),因为n=0,故执行fun(m,n)即等
				   价于fun(2,0),因此打印结果为:x=0。
a.fun(3)-----------执行结果为:x=0,原因同a.fun(2)。

var b = fun(0).fun(1).fun(2).fun(3);

相当于a.fun(1),返回值为fun(m,n),即fun(1,0),故结果为:    相当于c.fun(3),返回值为fun(m,n),即fun(3,2),
x=0。此次执行函数fun(1,0)后返回值为一个新的包含fun(m)     故结果为: x=2。此次执行函数fun(3,2)后返回值
方法的对象,假设为b,同样新对象的局部变量n值不会因为函      为一个新的包含fun(m)方法的对象,并将该对象最终
数fun(1,0)执行结束而销毁,此时n=1。                       赋值给全局变量b。
                                              .             .
                                              .             .
                                              .             .
                                     fun(0).fun(1).fun(2).fun(3)
                                       .             .
                                       .             .
                                       .             .
相当于fun(0,undefined),故结果为:x=undefined    相当于b.fun(2),返回值为fun(m,n),即fun(2,1),故结果    
返回一个包含fun(m)方法的新对象,假设为a,由于      为:x=1。本次执行返回函数fun(2,1)之后返回值为一个新
a对象的fun(m)方法中访问了其外层函数中的局部变     的包含fun(m)方法的对象,假设为c,同样新对象的n值不会
量n,因此n=0将保存在内存中,不会在函数fun(0)      因为函数fun(2,1)执行结束而销毁,此时n=2。
执行结束后销毁;

var c = fun(0).fun(1);

从全局变量b的结果,可知var c = fun(0).fun(1)的结果分别为:x=undefined 和 x=1。
从全局变量a的结果,可知c.fun(2);以及c.fun(3);的结果均为:x=1。

参考文献

原文链接:http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值