闭包的定义:
闭包是指有权访问另一个函数作用域中的变量的函数。
函数都是有作用域的,一般而言,函数只能操作自己作用域内的变量,不能干预别人的作用域。然而闭包就是一个特例。
变量的作用域
要理解闭包,首先必须理解js特殊的变量作用域。变量的作用域无非就是两种:全局变量和局部变量。js语言的特殊之处,就在于函数内部可以直接读取全局变量,而在函数外部无法读取函数内的局部变量。
需要注意的是,在函数内部声明变量的时候,一定要使用var命令。如果不用的话,实际上声明了一个全局变量。
创建闭包
常见方式为在一个函数内部创建另一个函数。
function aaa(){
var a=1;
return function(){
alert(a++);
}
}
aaa();//1 执行后a++,然后a还在
aaa();//2 闭包会使变量始终保存在内存中
上面这样在一个函数内部嵌套一个匿名函数就已经算是一个闭包。
下面我们就来具体说一说如何在函数外取到函数内的变量。
function outer(){
var x=10;
return function(){ //函数嵌套函数
x++;
alert(x);
}
}
var y=outer(); //将内部返回的函数赋给y,相当于:
//y= function(){
// x++;
// alert(x);
//}
y(); //y函数第一次调用,相当关于outer()();结果为11
y(); //y函数第二次调用,结果为12,实现了累加
在上述代码中,我们在outer函数的外部,通过返回outer内部嵌套的匿名函数来获取outer作用域内的变量x。那么,具体的实现原理是什么呢?
当某个函数被调用时,会创建一个执行环境,及相应的作用域链。我个人理解为,每一个函数在执行的时候会,都会造一个小房子把自己包在里面,这个小房子就是执行环境,然后小房子里面有一个整齐摆放的工具箱,那就是作用域链。函数在小房子里面工作(执行)的时候,需要操作的数据(也就是变量),都在工具箱里按照严格的等级以此摆放。那么那些等级又是怎么划分的呢?像形参、函数内局部变量、arguements等这些都称为活动对象。而这个函数作用域链上的活动对象不仅是自己的,还有父亲的,祖父的。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动和对象处于第三位……自己的活动对象永远都是第一位的。函数执行完成后,小房子被销毁,活动对象该释放的就会释放掉。
outer所包含的匿名函数在自己的作用域中访问了外部函数outer中的变量x,而这个匿名函数被返回了,并且在其他地方被调用了。但它仍然可以访问变量x,因为该匿名函数的作用域链中包含outer的作用域。我们就是利用这个特性在外部获取了outer作用域内的变量x。
闭包的三个特性:
- 函数嵌套函数
- 函数的内部可以引用外部的参数和变量
- 参数和变量不会被垃圾回收机制回收
闭包与索引值问题:
var result=new Array;
+function createFunction(){
for(var i=0;i<10;i++){
result[i]=function(){
return i;
};
}
return result;
}();
alert(result[0]());//10
alert(result[1]());//10
从表面来看,似乎每个函数都应该返回自己的索引值,即最后调用每一个result[i]并alert,感觉会弹出0~9,但实际会弹出10个10。因为实际的执行的时候,循环只会给每一个result[i]附一个函数,里面的i并不会同时赋值,只有在真正执行这个函数的时候,也就是要用到i的时候才开始给它赋值。而这个时候,循环已经跑完,i已经=10,所以会是10个10。
我们可以通过在函数表达式内部创建另一个匿名函数强制让闭包的行为符合预期。
var result=new Array();
+function createFunction(){
for(var i=0;i<10;i++){
result[i]=function(num){
return function(){
return num;
};
}(i);
}
return result;
}();
alert(result[0]());//0
alert(result[1]());//1
由于函数参数是按值传递的,所以会将变量i的当前值复制给参数num。而在这个匿名函数内部又创建并返回了一个访问num 的闭包。这样一来,result数组中每个函数都有自己num变量的一个副本,因此可以返回不同是数值。
闭包的使用场景
- 创建命名空间
//全局变量,test1是全局变量
var test1=111
function outer(){
alert(test1);
}
outer(); //111
alert(test1); //111
//闭包,test2是局部变量,这是闭包的目的
//我们经常在小范围使用全局变量,这个时候就可以使用闭包来代替。
(function(){
var test2=222;
function outer(){
alert(test2);
}
function test(){
alert("测试闭包:"+test2);
}
outer(); //222
test(); //测试闭包:222
})();
alert(test2); //未定义,这里就访问不到test2
- 给对象设置私有属性并利用特权(Privileged)方法访问私有属性
var Foo = function(){
var name = 'fooname';
var age = 12;
this.getName = function(){
return name;
};
this.getAge = function(){
return age;
};
};
var foo = new Foo();
foo.name; // => undefined
foo.age; // => undefined
foo.getName(); // => 'fooname'
foo.getAge(); // => 12
- 在函数外或其他函数中访问某一函数内部的变量
function f1(){
var test=111;
tmp_test=function(){return test;} //tmp_test是全局变量,这里对test的引用,产生闭包
}
function f2(){
alert("测试一:"+tmp_test());
var test1=tmp_test();
alert("测试二:"+test1);
}
f1();
f2();
//测试一:111
//测试二:111
alert(tmp_test()); //111
tmp_test=null;
4.引用传参后的函数
//无法传参的情况
var parm=222;
function f1(){alert(111)}
function f2(obj){alert(obj)}
setTimeout(f1,500);//正确,无参数
setTimeout(f2,500);//undefined,没有传参
setTimeout(f2(parm),500);//计时器无效,引用失败
setTimeout((function(parm){alert(parm)})(parm),500);//计时器无效,引用失败
//正确做法,使用闭包
function f3(obj){return function(){alert(obj)}}
var test2=f3(parm);//引用f3执行后返回的内部函数
setTimeout(test2,500);//正确,222
document.getElementById("hello").onclick=test2;//正确,222
document.getElementById("hello").attachEvent("onclick",test2);//正确,222
5.记住索引
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i);
}
总结:
虽然闭包可以通过另一个函数访问这个函数的局部变量。但由于闭包会携带包含他的函数的作用域,因此会比其他函数占用更多的内存,由于不同的垃圾收集方法,使用不当会造成内存泄露,也就是无法销毁驻留在内存中的元素。