闭包是一个比较抽象的概念,但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的理解以及js内部解释器的运作方式的描述,都是可以看出你js实际水平的.即使你没答对,也能让考官对你的水平有个评估.那么我先来说说我对js中的闭包的理解.
闭包是很多语言都具备的特性,在js中,闭包主要涉及js几个其他的特性:作用域链,垃圾(内存)回收机制,函数嵌套,等等.
在理解闭包以前.最好能先理解一下作用域链的含义,简单来说,作用域链就是函数在定义的时候创建的,用于寻找使用到的变量的值的一个索引,而他内部的规则是,把函数自身的本地变量放在最前面,把自身的父级函数中的变量放在其次,把再高一级函数中的变量放在更后面,以此类推直至全局对象为止.当函数中需要查询一个变量的值的时候,js解释器会去作用域链去查找,从最前面的本地变量中先找,如果没有找到对应的变量,则到下一级的链上找,一旦找到了变量,则不再继续.如果找到最后也没找到需要的变量,则解释器返回undefined.
了解了作用域链,我们再来看看js的内存回收机制,一般来说,一个函数在执行开始的时候,会给其中定义的变量划分内存空间保存,以备后面的语句所用,等到函数执行完毕返回了,这些变量就被认为是无用的了.对应的内存空间也就被回收了.下次再执行此函数的时候,所有的变量又回到最初的状态,重新赋值使用.但是如果这个函数内部又嵌套了另一个函数,而这个函数是有可能在外部被调用到的.并且这个内部函数又使用了外部函数的某些变量的话.这种内存回收机制就会出现问题.如果在外部函数返回后,又直接调用了内部函数,那么内部函数就无法读取到他所需要的外部函数中变量的值了.所以js解释器在遇到函数定义的时候,会自动把函数和他可能使用的变量(包括本地变量和父级和祖先级函数的变量(自由变量))一起保存起来.也就是构建一个闭包,这些变量将不会被内存回收器所回收,只有当内部的函数不可能被调用以后(例如被删除了,或者没有了指针),才会销毁这个闭包,而没有任何一个闭包引用的变量才会被下一次内存回收启动时所回收.
也就是说,有了闭包,嵌套的函数结构才可以运作,这也是符合我们的预期的.然后,闭包还有一些特性,却往往让程序员觉得很难理解.看看下面一段代码.
var result=[];
function foo(){
var i= 0;
for (;i<3;i=i+1){
result[i]=function(){
alert(i)
}
}
};
foo();
result[0](); // 3
result[1](); // 3
result[2](); // 3
这段代码中,程序员希望foo函数中的变量i被内部循环的函数使用,并且能分别获得他们的索引,而实际上,只能获得该变量最后保留的值,也就是说.闭包中所记录的自由变量,只是对这个变量的一个引用,而非变量的值,当这个变量被改变了,闭包里获取到的变量值,也会被改变.
解决的方法之一,是让内部函数在循环创建的时候立即执行,并且捕捉当前的索引值,然后记录在自己的一个本地变量里.然后利用返回函数的方法,重写内部函数,让下一次调用的时候,返回本地变量的值,改进后的代码:
var result=[];
function foo(){
var i= 0;
for (;i<3;i=i+1){
result[i]=(function(j){
return function(){
alert(j);
};
})(i);
}
};
foo();
result[0](); // 0
result[1](); // 1
result[2](); // 2
在这里我再解释一下.这里用到了另外2个技术,立即调用的匿名函数和返回函数.也是初学者比较难以理解的部分.
function a(){
var i=0;
function b(){
alert(++i);
}
return b; //返回b函数本身内容,不能写成return b()这样直接执行了
}
var c = a();
c();
这是个标准的闭包,这段代码有两个特点:
1、函数b嵌套在函数a内部;
2、函数a返回函数b。
var c = a();
c();
这两句执行很重要
这样在执行完var c=a()后,变量c实际上是指向了函数b,再执行c()后就会弹出一个窗口显示i的值(第一次为1)。所有的生命周期按理论来说就算全部结束. 第一次c()后,a中的i依然保留.自然a在内存的栈区依然保留.a是return过了,但是,a及内部值却依然存在,这就是闭包.
这段代码就创建了一个闭包,为什么?因为函数a外的变量c引用了函数a内的函数b,就是说:
当函数a的内部函数b被函数a外的一个变量引用的时候,就创建了一个闭包。
总结下,
1,闭包外层是个函数.
2,闭包内部都有函数.
3,闭包会return内部函数.
4,闭包返回的函数内部不能有return.(因为这样就真的结束了)
5,执行闭包后,闭包内部变量会存在,而闭包内部函数的内部变量不会存在.
再来案例
案例一:function aa(){
var b=10;
return function cc(){
b++;
alert(b);
}
}
aa()();
这个函数调用时,aa()(),有两个括号,第一个是调用aa函数,第二个是执行cc函数。
function test(){
return function(){alert("不做死就不会死!")}
}
test()();
第一个括号 执行test函数 返回子函数,第二个括号执行test返回的函数
为什么后面还要加一个括号,以前我直接test()这样调用,但是没有弹出结果,也没有报错。
后来在网友的提示下:再加一个括号,就可以了,注意直接test() 它返回的是子函数的内容,并没有调用子函数,不信你可以输出一下:
alert(test()) ,结果: ,再加上括号,就调用了:
如果觉得很难理解,你可以把它想象返回的不是一个函数,而是一个字符串,
:比如:function test(){
return alert("a+b");
}
test(); 结果:
还有子函数里为什么要写return,?这是因为要在父函数外部调用。看下面这段代码;
如果不想用两个括号调用,有两种方式。一是定义一个变量接收子函数返回的值,再执行变量所在的函数,二是子函数直接在里面就调用。
还是拿案例一来说:
可以改成第一种方式。
function aa(){
var b=10;
return function cc(){
b++;
alert(b);
}
}
var dd=aa();
dd();
二: function aa(){
var b=10;
(function cc(){
b++;
alert(b);
})();
}
alert(aa());
结果:11,undefined
为什么第二个会弹出undefined,因为:如果一个函数没有返回值,则会留下一个undefined
注意如果内部函数在里面执行,那么前面就不要写return, 如:return (functioncc(){ b++; alert(b); })(); 此时return在里面没有意义,因为没有返回值 ,就不要写return,就像java 不会写returnvoid一样。函数运行就是个闭包,如果里面的子函数不在里面执行,就要加上return ,然后在父函数外面调用返回的子函数。
下面这个问题,点击第个li,得到其索引。
问题:里面子函数并没有执行,为什么也能弹出结果?
代码如下:
window.οnlοad=function(){
var li=document.getElementsByTagName("li");
for(var i=0;i
1
2
3
4
5
像上面那些写法都是要么在里面加上括号,直接调用,要么在父函数外面执行。而这里却没有?
解释:上面的内部的函数被绑定到事件上了
父函数运行,然后把里面的函数返回了,然后返回给绑定的事件上
这时代码就变成这样:
li[i].οnclick= function(){
alert(n);
}
这是我们常用的写法,很明显,这样就运行了子函数,就会弹出结果。这个闭包还有第二种写法:
window.οnlοad=function(){
var li=document.getElementsByTagName("li");
for(var i=0;i
因为要用到循环里的变量,所以用一个闭包把下面的代码包起来,并传给一个形参n,调用时传实参i,这个i就是for循环里的i。
有一种格式颇受争议:
(function(a,b))(a,b);闭包有许多有趣的用途,Javascript的两个特征使它这么有趣:1. function是一个对象,它跟数组,Object一样,地位平等。2. Javascript变量作用域范围。《Javascript权威指南》对这两点有深入的讲解。
闭包有一个著名的用途就是实现面向对象的访问控制。也就是c++, c#, java等面向对象的private, public访问控制。先看一段示例代码
上面的代码很简单,只是把数据成员a的访问器(setter/ getter),放在了构造函数中。a使用var声明,外面不能直接访问a。get_a和set_a都用this来使得这两个函数与类对象关联,并且外面可见。
首先,我们使用原型(prototype)来定义一个类
从上面的运行结果可以看出,使用prototype定义的类,实例化出来的对象都共享同一个方法,一旦原型改变了,会影响全部对象。而一个对象修改了自身的方法,系统会执行copy on write,把对象指向新的方法而不会影响其他对象。
再看看以下这一段:
从运行结果可以看到,闭包的结果是每个对象都拥有独立的方法,即使对象之间的方法的实现一模一样。这样会造成多余的内存浪费。