首页 Javascript 正文
265

Javascript之闭包

  • yiqingpeng
  • 2015-04-29
  • 0
  •  
刚开始接触闭包这个概念的时候,很难理解,于是闭包这个问题就一直萦绕在我脑海里。
每当看书或者其它资料时,闭包这个词出现的时候,我就会特别地关注一下,看看不同的人对闭包是如何解释的。
现在我就简单地谈一下自己的理解,可能不是很恬当。
先来看一个闭包的例子:
function parentFunc(){
    var p=1; //使用var定义局部变量,外部函数中的局部变量是闭包形成的必要条件之一。
   return function childFunc(){ //将内部函数抛出到外部执行环境中去是闭包的一个特征,这里我就不说是必要条件了,否则有人要喷我了。
           alert(++p); //内部函数中, 对外部函数的变量引用也是闭包形成的必要条件之一。
   }
}

var f = parentFunc();
f();//alert 2;//外部函数变量的值依然保存在内存当中。
f();//alert 3;
我这里估且先将函数parentFunc称之为父函数(独家创作的概念,正确的说法是外部函数),称 childFunc为子函数(标准说法是内部函数)。那么上面的例子中,子函数就形成一个闭包:子函数拥有了父函数中变量P的控制权,并且在脱离父函数的运行环境之后,依然具有对变量P的访问权限。注意变量P一定是父函数中定义的局部变量。

通过使用chrome的开发工具,可以窥视到闭包的本质, 注意func1和func2这两个函数[[Scopes]]属性的差异,这就是闭包的关键所在。func1的作用域链:local => Global. 而func2的作用域链:local => withClosureFunc => Global. 说到这里就不得不提到一本书《JavaScript高级程序设计(第3版)》(封面是看天文镜的小男孩),此书也算是javascript的经典之作,确实不错。但是在闭包这一章节,不知道是不是翻译的问题,它对闭包的描述字里行间表达的意思是,即使内部函数没有引用外部函数的任何变量也会将外部函数的作用域添加到内部函数的作用域链前端,这似乎与chrome开发工具看到的情况不相符。


通过我这提供的这个例子得到一个感性认识之后,建议大家还是得去参考一下正统的说法,比如Closures-MDN

再来一个闭包的用法示例:
如何向定时器调用的函数传参数?
我们知道,像setTimeout之流,第一个参数是一个函数句柄,不能传入参数,于是有人就这样做了:
function funcName(para){alert(para);}
setInterval("funcName('hello')",1000);//如果是这样书写的呢:setInterval(funcName('hello'), 1000);//发现只会打印一次"hello", 因为funcName('hello')是在调用函数,它的返回值作为了计数器的参数,而不是函数本身作为参数。要实现动态赋值,可以像这样setTimeout("funcName('"+myVar+"')", 1000);//实现动态传递参数。
这样确实可以达到一定的目的,但是参数不好随计数器周期性的改变。那我们可以用闭包来解决:
function funcName(para){para--;console.log(para);}
setInterval(function(){var myVar=10;funcName(myVar);}, 1000);//每隔一秒就将变量myVar减一吗?发现并没有,而是每隔一秒输出的都是9,那么如何输出9,8,7...呢,继续看下去。

其实是这样的:
function funcName(para){
     return function(){para--;console.log(para);}//这里就是一个典型的闭包,当然这里可以return一个匿名函数,也可以是一个有名称的函数,比如像return function func(){...}
}
setInterval(funcName(12); 1000);//funcName(12)返回值是一个闭包函数,这个函数作为了计数器的回调函数。代码运行就会输出11,10,9,...。这也从另一个侧面反映出闭包对外部函数变量的继承(变量仍然在保持在内存中,并受闭包函数控制。)

我们再来看一个例子:
var queue  = [];
for(var i=0;i<3;++i){
  queue.push( function(){ console.log(i);} );
}
queue[0]();//out 3
queue[1]();//out 3
queue[2]();//out 3
三次循环中压入队列的三个函数,本来想依次打印出循环中递增变量i每次的值,结果输出的都是i的最终值3。说明这样的做法并不能保留每次循环的现场。我们利用闭包特性稍加修改就可以达到我们的目的:
var queue  = [];
for(var i=0;i<3;++i){
  queue.push( (function(p){ return function(){console.log(p)}; })(i) );
}
queue[0]();//out 0
queue[1]();//out 1
queue[2]();//out 2


除了在函数作为另一函数的返回值时形成闭包之外,还可以有下面这种形式:
var x='x of out';
function out(){
  alert(x);  
  x = 'x of out modyfied';
}

(function(func){
    var x = 'x of in';
    func();    //alert 'x of out';
    func();    //alert 'x of out modyifed';
    alert(x);  //alert 'x of in';
})(out);

在结束的时候,还要来一个例子,这个例子是一个形式上像闭包,但却不是闭包的用法。


var m = 'out';
function func(){
    var m ='in';
    return new Function('return m;');
}
var f = func();
alert( f() );//alert ‘out’, not ‘in’,为什么呢,我将在下一节讨论匿名函数时解释。



正在加载评论...