在学习闭包之前,要先了解Javascript中作用域(scope)的相关概念:
一,变量作用域(Gloabl & Local)
1.全局变量能在任何地方被访问;
1 var a = 8; 2 function a(){ 3 alert(a); 4 } 5 a(); //8 6 //假如改写下面一种函数 7 function a(){ 8 alert(a); 9 var a = 1; 10 alert(a); 11 } 12 a(); //undifined, 1
后一种情况,在a()的作用域中,变量a被重写(rewrite),故第一个alert的时候提示a未定义.
2.定义在函数内的变量不能被函数外的程序访问到
1 function a(){ 2 var a = 1; 3 } 4 alert(a);//undefined
3.定义在函数块(if or for loop...)中的变量对外部(第一层函数体)是可见的
1 function f1(){ 2 var a = 1; 3 for(var i=1;i<10; i++) 4 a++; 5 function f2(){ 6 if(true) 7 var b = 8; 8 } 9 alert(i);//10,i在f1()范围内都是可见的 10 f2(); 11 alert(b);//undefined,b的可见性止于f2(); 12 } 13 f1(); 14 alert(i);//undifined,i的可见性止于f1();
4.若函数f1()中嵌套另一个函数fn(),则f2()能访问的变量将是它自己内部定义的加上父级函数(这里是:f2())的变量.
1 var a = 1; 2 function f1(){ 3 var b =2; 4 alert(a); //1 5 function f2(){ 6 var c =3; 7 alert(b); //2,alert(a);//1 8 function f3(){ 9 var d =4; 10 alert(c); //3,alert(b);//2,alert(a);//1 11 ... 12 function fn(){ 13 14 } 15 } 16 } 17 }
当然,如果你喜欢.可以一直嵌套下去,这个规则是一直适合的.
二,函数作用域(Lexical Scope)
在函数被定义的时候(不是执行的时候),函数的"环境"就被创建.
1 function f1(){ var a =1; f2();} 2 function f2(){ return a; } 3 //error: a is not defined .当f2被定义(不是执行)的时候,在他自己的作用域和Gloabl scope中并未发现a. 4 f1();
当一个函数被定义的时候,他就"记住"了他所处的环境--作用域链;就像上面的程序,f1(),f2()都是定义的全局函数,因此他们的作用域都是Gloabl Scope.因此当f2();执行的时候自然不能访问f1()内部的变量.这时两者的作用域分别为:
1.f1();全局变量+自己内部变量;
2.f2();只能访问全局范围内的变量.
同样地:做如下的修改
1 function f1(){ var a =1; return f2();} 2 function f2(){ return a; } 3 //error: a is not defined. 4 f1();
同上:此时的f1,f2所处的环境都是Window,属于全局函数,f2自然也不能访问到同级别的f1中的变量.在f2定义的时候,并没有一个全局变量叫'a'的....
做如下改动:
1 var = 5; 2 f1(); //5 3 a = 55; //缺省的全局变量 4 f1(); //55 5 delete a;//true 6 f1(); // a is not defined 7 delete f2();//true 8 f1(); //f2 is not defined 9 //重新定义下f2; 10 var f2 = function() { return a*2; } 11 var a = 5; 12 f1(); //10
当声明一个全局变量a后,f2()就可以正常工作了,因为f2记住了它所处的"环境"--Global Scope,并且它能访问到一切声明在全局的变量.就像这里的a.
故可得出以下结论:
当函数被定义的时候,只是记住了当前自己所处的"环境"--作用域.当函数执行的时候,按照自己能访问的域寻找相应的变量或则函数.注意,这里只能是自己有权访问的范围!(作用域链)
三.闭包--打破上面的作用域链限制图(1)
我们把Gloabl Scope想成是整个宇宙,当然他包括一切事物.在这里,它包含变量(a),还有函数(F).
图(2)
在全局范围下,每个函数有自己的私有空间,他们能用这个空间存储一些变量,甚至是函数.
图(3)
如上图所示:有一个全局环境G,一个全局变量a,全局函数F,以及F中定义的变量b,还有F中私有的方法N和N自己的变量c.则他们的访问规则如下:
最里层的函数能访问到外层变量,反之则不行.
有趣的时,当引入这样一个函数N后,这样的规则将会被打破!---闭包
图(4)
我们来分析一下,在这里F,N处在同样的外部环境,大家都是全局函数,他们都能记住自己被定义时的环境.
另外的:N还能访问到F-space.故也能访问到变量b,这非常的有趣,因为a和N的位置一样.但是只有N能访问到b,而a却不能!---N打破了传统的作用域链.
闭包是怎么形成的?
1.可以在图(3)所示的情况中,定义N是忽略掉关键字var.(定义成了全局的函数).这样N具有双重身份,既是全局函数,但又能访问到F的空间!
1 function f1(){ 2 f2 = function() { 3 // 4 } 5 }
2.通过F把N传递(return)到全局空间.
1 function f1(){ 2 // 3 return function f2(){ 4 // 5 } 6 }
实例1:
1 function f(){ 2 var b = "b"; 3 return function(){ 4 return b; 5 } 6 }
声明了一个全局函数f(),有一个局部变量b;返回值是一个函数(匿名);把这个返回函数想象成上图中的N.它能访问到特有的环境--"f()的空间"+"全局空间";故它能访问到b;因为f是一个全局函数.你可以这样使用他:
1 var n = f(); 2 n();
神奇的事就这么发生了,这里的新函数n()能访问到f的私有空间!
实例2:
1 var n; 2 function f(){ 3 var b = "b"; 4 n = function(){ 5 return b; 6 } 7 } 8 n(); //"b"
So:当我们调用f();的时候会发生什么情况?
一个新的函数n被定义在f里面,不幸的是.定义的时候忘记了加关键字var.导致n变成了全局函数.在定义的时间内,n()都是在f()内部的,所以n()尽管作为全局函数,但也能访问到f的私有变量或则函数.
实例3:
THEN: What's Closure?
从我们上面的这么多讨论可以得到一个比较通俗的理解:闭包就是一种方法:想办法(return/gloabl function)让外界访问到父辈的私有变量.
1 function f(arg) { 2 var n = function(){ 3 return arg; 4 }; 5 arg++; 6 return n; 7 } 8 var m = f(23); 9 m(); //24
让外界实时感知f()内部私有变量的变化
实例4.循环结构中的闭包
循环结构中的闭包是很容易出现BUG的,如果不注意使用:特别是对概念理解不强的情况下.如下:
1 function f() { 2 var a = []; 3 var i; 4 for(i = 0; i < 3; i++) { 5 a[i] = function(){ 6 return i; 7 } 8 } 9 return a; 10 }
在f()中我们通过for()循环每次产生一个新的函数(闭包),然后用数组a记录下函数.最后返回a.试想一下a中的内容会是什么?接下来就是见证奇迹的时候....
1 var a = f(); 2 a[0]; //3 3 a[1]; //3 4 a[2[; //3
哇!怎么会这样???你还可以通过这里了解更多
原理:我们通过循环创建了三个闭包,但是所有的闭包都是指向变量i的.由于前面说过,函数定义的时候只是记住了当前所处的环境,并没有记住自己范围内所有变量的值(这些值是可以随时增加/修改/删除的).他们做的都是同一件事---指向i.故在循环结束以后,i都变成了3.这就是产生以上结果的最终原因.
如何改进:
就上面产生的原因来讲,我们只要让三个闭包拥有不同的指向,这个问题就能解决.
1 function f() { 2 var a = []; 3 var i; 4 for(i = 0; i < 3; i++) { 5 a[i] = (function(x){ 6 return function(){ 7 return x; 8 } 9 })(i); 10 } 11 return a; 12 }
是的,在这里通过self-invoking的方式,在每个闭包刚定义的时候就已经执行了---等等....这个执行的意思是:此时的x并不是一个指向了,而是一个真正的值!
PS:self-invoking大概是如下意思:
1 (function() { //... })(); 2 //第一个()里面是函数主体.第二个括号的意思是执行...第二个()中的参数就是传递给这个函数的参数
改进后的执行结果将是:
1 var a = f(); 2 a[0]; //0 3 a[1]; //1 4 a[2]; //2
还有一中改进方法:
另外声明一个构造函数:
1 function makeClosure(x) { //构造函数 2 return function(){ 3 return x; 4 } 5 } 6 7 function f() { 8 var a = []; 9 var i; 10 for(i = 0; i < 3; i++) { 11 a[i] = makeClosure(i); 12 } 13 return a; 14 }
实例5.Getter/Setter
想象一个这样一个场景:你拥有一组比较特殊的变量,你不想他们暴露,并且也不希望其他的任何部分代码改变他们的值.方法是:
在这个函数里面提供额外的两个函数---一个用来获取他们的值(gettter),另一个用来设置他们的值(setter).当然,设置值的函数还应该包含一些必要的验证措施.
把函数getter和setter都放在含有特殊变量的函数里面,即他们共享相同的环境--作用域.
1 var getter, setter; 2 (function() { 3 var secret = 0; 4 getter = function(){ 5 return secret; 6 }; 7 setter = function(v){ 8 secret = v; 9 }; 10 })()
在这个匿名函数里面,定义了两个全局的函数getter和setter,secret是一个无法被外界直接访问的私有变量.将会有如下的工作过程:
1 getter(); //0 2 setter(88); 3 getter(); //88
实例6:迭代器:展示用一个闭包来完成迭代器的功能
试想一下你有一个非常复杂的数据结构,你想遍历里面的每一项内容,我们实现一个next()方法去遍历他.
下面这个函数接受一个数组输入,然后在函数内部定义一个私有的指针i,i总是指向数组中的下一个元素.
1 function setup(x) { 2 var i = 0; 3 return function(){ 4 return x[i++]; 5 }; 6 }
我们这样调用setup()的时候将会是这样的结果:
1 var next = setup(['a','b','c']); 2 next(); //"a" 3 next(); //"b" 4 next(); //"c" 5 next(); //undefined
注意,这里有一个问题:setup()函数中的私有变量i是一直存在内存中的!因为闭包会一直依赖它所处的环境,故父函数的变量会一直存在在内存中,滥用闭包将有可能引起内存泄漏!所以在退出闭包前,需要清空不必要的局部变量.