什么是闭包
- 一种写法
- 在函数定义处的环境中自带数据
- 一种为局部定义函数封装信息的方式
闭包热身
普通循环
for (var i = 0; i < 5; i++) {
console.log(i);
} //输出0 1 2 3 4
延时循环
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000 * i);
} // 因为1秒后循环已经结束输出5个5
让延时循环输出0到4 (使用闭包)
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i); //一个立即执行函数,用到了闭包,输出0 1 2 3 4,每次循环都把i保存了下来
}
热身结束,这其实是来自知乎上面一道关于JS运行机制的文章的前半段,运行机制以后再说,有兴趣的话可以先看 运行机制
一,变量作用域
变量作用域有两种:全局变量和局部变量。JavaScript语言规定,在函数内部可以直接读取全局变量。
var n=999;
function f1(){
alert(n);
}
f1(); // 999
另一方面,在函数外部自然无法读取函数内的局部变量。
function f1(){
var n=999;
}
alert(n); // error
注意,函数内部声明变量的时候,一定要使用var命令。如果不使用,实际上声明了一个全局变量。
function f1(){
n=999;
}
f1();
alert(n); // 999
二,如何从外部获取局部变量
如何从外部获取局部变量,那就是在函数的内部,再定义一个函数。
function f1(){
var n=999;
function f2(){
alert(n); // 999
}
}
在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。对上面例子变通下。
function f1(){
var n=999;
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
上面代码中,把函数f2作为返回值,那在函数f1外部,就可以获取它内部变量了。
三,闭包
上面代码中,函数f2,就是闭包。闭包就是能够读取其他函数内部变量的函数。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。闭包作用有两个:
- 可以读取函数内部的变量
- 这些变量始终保持在内存中
例一:
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
nAdd();
result(); // 1000
上面代码中,执行函数f1
返回函数f2
,result = f2
,result
实际上就是闭包f2函数,它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
为什么会这样呢?原因就在于f1
是f2
的父函数,而f2
被赋给了一个全局变量,这导致f2
始终在内存中,而f2
的存在依赖于f1
,因此f1
也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。(具体阅读《JavaScript-内存》一章)
另外,nAdd=function(){n+=1}
nAdd
是一个全局变量,nAdd
的值是一个匿名函数,这个函数本身也是一个闭包。
四,使用闭包注意点
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
例子:
function assignHandler() {
var el = document.getElementById('demo');
el.onclick = function() {
console.log(el.id);
}
}
assignHandler();
以上代码创建了作为el
元素事件处理程序的闭包,而这个闭包又创建了一个循环引用,只要匿名函数存在,el
的引用数至少为1,因些它所占用的内存就永完不会被回收。
function assignHandler() {
var el = document.getElementById('demo');
var id = el.id;
el.onclick = function() {
console.log(id);
}
el = null;
}
assignHandler();
把变量el
设置null
能够解除DOM对象的引用,确保正常回收其占用内存。
五,模仿块级作用域
任何一对花括号({和})中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。
(function(){
//块级作用域
})();
六,实际例子
例一:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());
上面代码中,执行object.getNameFunc()
返回 function(){return this.name}
在执行一次,相当于执行这个返回函数,得到 this.name
这里的this
代表window
,所以是全局变量 name
例二:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()());
上面代码和例一的区别是,把this保存起来,that
代表 object
,得到的是 'My Object'
例三:(出自很多人都会做错的闭包题)
function fun(n,o) {
console.log(o);
return {
fun:function(m){
return fun(m,n);
}
};
}
var a = fun(0);a.fun(1); a.fun(2); a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1); c.fun(2); c.fun(3);//undefined,?,?,?
/*
解题前,需要知道第一个函数fun(n,o),第二个函数 fun: function(m){return fun(m,n)},第三个函数 fun(m,n) 其中第一和第三函数相同
匿名函数有 var fn1=function (){} ,具名函数 function fn1(){} 和 var fn1=function a(){};
*/
/* 第一个
1, var a = fun(0); ---->执行fun(0)后, 返回一个对象 a = {fun:function (m){return fun(m,n)}} ,此时 n = 0, o = undefined
2, a.fun(1); -----> 返回一个函数fun(m,n),此时 m = 1, n = 0 由于闭包前面的变量不会被删除,所以fun(1,0) 执行后 console.log(o) = 0;
3, a.fun(2), a.fun(3) 和上面一样的结果
*/
/*第二个:链式调用,后面执行的函数调用前面返回的数据
1, var b = fun(0) 和上面第一步一样, 返回 {fun:function (m){return fun(m,n)}} 和 n = 0, o = undefined
2, 执行到 fun(1) 时, 返回 fun(m,n) , 此时 m = 1, n = 0 , 执行 fun(1,0) 后 console.log(o) = 0 返回 {fun:function (m){return fun(m,n)}} ,此时 n = 1, o = 0
3, 执行到 fun(2) 时, 返回 fun(m,n) , 此时 m = 2, n = 1 , 执行 fun(2,1) 后 console.log(o) = 1 返回 {fun:function (m){return fun(m,n)}} ,此时 n = 2, o = 1
4, 执行到 fun(3) 时, 返回 fun(m,n) , 此时 m = 3, n = 2 , 执行 fun(3,2) 后 console.log(o) = 2 返回 {fun:function (m){return fun(m,n)}} ,此时 m = 3, o = 2
*/
/*第三个
1, var c = fun(0).fun(1); 执行和第二个前两步骤一致,o = undefined 和 0
2, 执行到 c.fun(2)时,c = {fun:function (m){return fun(m,n)}} ,此时 n = 1, o = 0, 返回 fun(2,1)后 m = 2, n = 1 ,o = 1;
c.fun(3) 执行出来一致 o = 1;