闭包是javascript的一个难点,也是一个重点,很多高级应用都需要用到闭包。在学习闭包的过程中看了不少书和大牛的博客,下面来说说我对闭包的理解和看法。
函数
要说闭包就不得不先说一下函数,这个javascript的中流砥柱。
众所周知在ES6之前js都没有class这个关键字,一切的面向对象都是通过function模拟的。
一个简单到不能再简单的例子:函数内部可以访问外部的变量(全局变量)。
var n=100;
function f1()
{
console.log(n);
}
f1();//100
而外部却不能访问函数内的局部变量。
function f1()
{
var n=100;
}
console.log(n);//n is not defined
那么问题来了,如果我想访问函数内的局部变量怎么办?一种解决办法就是使用闭包。
什么是闭包?我个人认为函数中的函数就是闭包,有点像java的内部类,既然函数可以访问外部变量,那么函数中的函数也可以访问外层函数的变量,这个内层函数就是闭包。
function f1()
{
var n=100;
function f2()
{
console.log(n);
}
f2();
}
f1();//100
上述写法比较麻烦,因此可以把f2作为一个返回值返回给f1。
function f1()
{
var n=100;
function f2()
{
console.log(n);
}
return f2;
}
f1()();//100
//相当于var f=f1();f();
下面来看一个问题:这里有5个li,名字是0到4,我希望我点击0的时候在控制台输出0,点击1的时候在控制台输出1,以此类推。
var lis=document.getElementsByTagName("li");
for(var i=0;i<lis.length;i++)
{
lis[i].onclick=function()
{
console.log(i);
}
}
奇怪的事情发生了,无论我点击哪一个在控制台输出的都是5。
这是为什么呢?
仔细观察,我为每个li创建了一个匿名函数,匿名函数形成了闭包,它们都在引用外部的i,当i改变时,自然所有匿名函数里面的i都改变了。用几张图来说明。
当i增加到5的时候不再满足循环条件,由于五个匿名函数都是引用同一个i,所以它们打印出来的都是5。
既然它们是引用同一个i,那么只需为每一个i创建一个备份:
var lis=document.getElementsByTagName("li");
for(var i=0;i<lis.length;i++)
{
lis[i].onclick=function(num)
{
return function()
{
console.log(num);
}
}(i)
}
这里在匿名函数接收一个参数并且立即执行,匿名函数内部又创建了一个函数负责把这个参数返回给匿名函数,由于是按值传递的,所以相当于给i做了一个备份。就算i一直在改变,而每个匿名函数的num是固定的。
闭包中的this
在闭包中使用this会出现一些问题,在全局函数中this指向的是window,当函数以方法被调用时,this就指向这个对象。但是,匿名函数的执行环境具有全局性,this一般指向window。参考红宝石书中的一个经典的问题:
var name="The Window";
var object={
name:"My Object",
getNameFunc:function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());//"The Window"
内部函数在搜索this的时候只会搜到其活动对象为止,由于匿名函数的执行环境具有全局性,因此当前的活动对象就是window。
可以通过改变当前的活动对象来改变this的指向:
var name="The Window";
var object={
name:"My Object",
getNameFunc:function(){
var that=this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()());//"My Object"
这里把this对象赋值给了一个that变量,因此当前的活动对象就从window变成了object。
闭包引发的问题
闭包虽好,但是也引发了一系列问题,尤其是内存泄漏。js的垃圾回收机制是引用计数的,当一个变量不再使用的时候(引用计数为0)垃圾回收机制就会把它清理掉。而闭包的存在会导致引用数至少为1。
function f()
{
var myDiv=document.getElementById("Div1");
myDiv.onclick=function()
{
console.log(myDiv.id);
};
}
上述代码中,函数f需要等myDiv清除后才能被清除,而myDiv由于匿名函数的存在,它的引用数至少为1,因此它所占用的内存永远不会被回收。
解决办法:
function f()
{
var myDiv=document.getElementById("Div1");
var id=myDiv.id
myDiv.onclick=function()
{
console.log(id);
};
myDiv=null;
}
把myDiv.id保存在一个变量中,并且在闭包中引用这个变量,这样就与myDiv没有关系了。但是闭包会引用包含函数的整个活动对象,因此必须把myDiv设为null来解除对它的引用,确保垃圾回收机制能够把资源回收。
使用闭包应该注意的问题:闭包虽然好,但是在实际应用中尽量减少 闭包的使用,并且为了防止内存泄漏,要时刻记得在闭包完后清除它的局部变量。