参考整理:http://www.cnblogs.com/zhangle/archive/2010/07/02/1770206.html
1.什么是闭包
之前讲过内部函数(Inner Function),在outer函数的外部是无法访问inner函数的。所以也称之为私有函数(private function);
但是在很多情况下,我们需要访问到内部的作用域。这个时候就需要闭包。
function f(){
var b="b";
return function(){
return b;
};
}
alert(f());
alert的结果是
function(){
return b;
}
因为返回了一个匿名函数,由此也说明函数也是数据
为了返回b;我们可以
var n = f();
n();//访问b
我们还可以用下面的方法来替代:
var n;
function f(){
var b = "b";
n=function(){
return b;
}
}
f();
alert(n());
通过上面例子我们就可以说当一个函数指向了它的父作用域,就可以称之为闭包。
官方对闭包的解释是:一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
闭包的特点:
1.作为一个函数变量的一个引用,当函数返回时,其处于激活状态。
2.一个闭包就是当一个函数返回时,一个没有释放资源的栈区。
简单的说,javascript允许使用内部函数---即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。
官方的解释估计新手没办法一下子看懂,其实闭包简单的说就是:
当函数a的内部变量b(就是数据,可以是对象或函数)被函数a外的一个变量引用的时候,就创建了一个我们通常所谓的“闭包”。
2.闭包的原理
如果要更加深入的了解闭包以及函数a和内部变量b的关系,我们需要引入另外几个概念:函数的执行环境(excution context)、活动对象(call object)、作用域(scope)、作用域链(scope chain)。以函数a从定义到执行的过程为例阐述这几个概念。
- 当定义函数a的时候,js解释器会将函数a的作用域链(scope chain)设置为定义a时a所在的“环境”,如果a是一个全局函数,则scope chain中只有window对象。
- 当执行函数a的时候,a会进入相应的执行环境(excution context)。
- 在创建执行环境的过程中,首先会为a添加一个scope属性,即a的作用域,其值就为第1步中的scope chain。即a.scope=a的作用域链。
- 然后执行环境会创建一个活动对象(call object)。活动对象也是一个拥有属性的对象,但它不具有原型而且不能通过JavaScript代码直接访问。创建完活动对象后,把活动对象添加到a的作用域链的最顶端。此时a的作用域链包含了两个对象:a的活动对象和window对象。
- 下一步是在活动对象上添加一个arguments属性,它保存着调用函数a时所传递的参数。
- 最后把所有函数a的形参和内部变量b的引用也添加到a的活动对象上。在这一步中,完成了b的的定义,因此如同第3步,b的作用域链被设置为b所被定义的环境,即a的作用域。
3.闭包在循环中的应用
在循环中很容易引起一些bug,虽然表面是正常的。
function f(){
var a = [];
var i;
for(i=0;i<3;i++){
a[i] = function(){
alert(i);
return i;
}
}
return a;
}
var a= f();
a[0]();//3
a[1]();//3
a[2]();//3
新建个循环,我们的目的是每次循环都新建一个函数,函数返回的是循环的序列值也就是i。我们看看以上代码的运行结果
都是为3.而我们期望的结果是1,2,3。
到底是为什么呢?我们新建了三个闭包,都指向了变量i。闭包并没有记住变量i的值,它仅是变量i的引用。在循环结束后,i的值是3,所以结果都是3了。
来看正确的方法:
function f() {
var a = [];
var i;
for(i = 0; i < 3; i++) {
a[i] = (function(x){
return function(){
alert(x);
return x;
}
})(i);
}
return a;
}
var a = f();
a[0]();//0
a[1]();//1
a[2]();//2
我们又用了一个闭包,把变量i变成了局部变量x了,x变量每次都是不同的值了。如果没有彻底理解自调用函数那就如下写法就可以明白了
function f() {
function makeClosure(x) {
return function(){
return x;
}
}
var a = [];
var i;
for(i = 0; i < 3; i++) {
a[i] = makeClosure(i);
}
return a;
}
4.常见应用
(1)setTimeout中使用参数
setTimeout() 是window对象的方法 但我们都是略去 window,
作用是设定一个时间, 时间到了, 就会执行一个指定的方法。
<html>
<body>
<script>
setTimeout("alert('对不起, 要你久候')", 3000);
</script>
</body>
</html>
打开页面之后,等待3s后弹出对话框
然后我们看一下以下这个例子:
<script language="JavaScript">
<!--
function say(words){
alert(words);
}
setTimeout(say("hello world"),1000);
setTimeout(say("I'm hungry,I'm need some food!"),2000);
//-->
</script>
这个例子是弹出一个窗口"hello world".
使用闭包,将上面的例子改成
<script language="JavaScript">
<!--
function say(words){
return function(){
alert(words);
}
}
setTimeout(say("hello world"),1000);
setTimeout(say("I'm hungry,I'm need some food!"),2000);
//-->
</script>
这个例子是弹出两个窗口"hello world", "I'm hungry,I'm need some food!"
为什么会这个样子?
第一个例子,实际上没有执行setTimeout函数,而是在它的第一个参数的执行say("hello world")
而第二个例子,借用闭包,setTimeout函数的第一个参数是匿名函数
function(){
alert(words);
}
没有直接执行,而通过setTimeout函数执行,从而实现了传参效果。
因此在setTimeout函数中,第一个参数必须传入参数的引用,
如果需要传入参数需要借用闭包
然后我们再看看所有的settimeout使用参数的方案:
/*
* 先从正常的延时执行说起,以下代码会在2s后弹出true,OK
*/
function st(){
alert(true);
}
setTimeout(st, 2000);
/*
* 下面的代码也会弹出true,但不OK,因为延时没有起作用
*/
function st(arg){
alert(arg);
}
setTimeout(st(true), 2000);
/*
* 解决方法之一,这种方法可以应付参数为字符串型的,对object型就不OK了
*/
function st(arg){
alert(arg);
}
setTimeout('st('+ true +')', 2000);
/*
* 解决方法之二,可以利用闭包
*/
function st(arg){
return function(){alert(arg);}
}
var st1 = st(true);
setTimeout(st1, 2000);
/*
* 解决方法之三,可以重载 window.setTimeout 函数,代码转载有修改,这里也有用到闭包的概念
*/
var _st = window.setTimeout;
window.setTimeout = function(fRef, mDelay){
if(typeof fRef == 'function'){
var argu = Array.prototype.slice.call(arguments,2);
var f = function(){
fRef.apply(null, argu);
};
return _st(f, mDelay);
}
return _st(fRef,mDelay);
}
function st(arg){
alert(arg);
}
setTimeout(st, 2000, true);
(2)将对象的方法与其他实例关联
比如我们在做一个多文件上传的程序,要求可以动态添加和删除文件:
function addFile(){
var fileInput = document.createElement("input");
fileInput.type = "file";
fileInput.name = "file";
var btnDel = document.createElement("input");
btnDel.type = "button";
btnDel.value = "删除";
btnDel.onclick = (function(){
//删除文件
return function(){
var c = _("fileContent");
c.removeChild(fileInput);
c.removeChild(btnDel);
}
})();
var fContent = _("fileContent")
fContent.appendChild(fileInput);
fContent.appendChild(btnDel);
}
在这个例子中,我们在删除按钮的点击时间关联的函数中删除对应的上传表单和按钮本身。由于表单和按钮都是动态生成,在事先并不知道删除按钮点击后需要删除的内容是什么。但是我们可以使用javascript闭包的特性,在上传表单和按钮生成的时候将按钮的点击事件与上传表单实例及和按钮实例进行关联。
(3)模拟静态私有变量
Javascript本身并不支持面向对象的特性。但是我们可以通过javascript的一些特性模拟实现javascript面向对象。
比如我们需要定义一个类,并且模拟静态私有变量。
var Class2 = (function(){
var s_var = 0; //静态私有变量
return function(){
this.getInstanceCount = function(){
return s_var;
}
//constructor
s_var++;
}
})()
var cls1 = new Class2();
alert(cls1.getInstanceCount()); //1
var cls2 = new Class2();
alert(cls1.getInstanceCount()); //2
alert(cls2.getInstanceCount()); //2
var cls3 = new Class2();
alert(cls1.getInstanceCount()); //3
alert(cls3.getInstanceCount()); //3
这个例子中,我们用s_var记录Class2被实例化的次数,使用闭包,我们可以将s_var模拟为一个静态私有变量,每次Class2被实例化的时候将s_var加1。
上例中我们使用了这样形式的一段代码,其中定义在外层函数内,内层函数外的成员类似于静态成员。所以这样形式的代码我们可以把他叫做“静态封装环境”。
(function(){
return function(){
}
})()