一、什么是闭包
官方解释:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。咋一看,这句话晦涩难懂,一脸懵逼。直接上代码,便于理解function test( ){
var a = 0;
return function( ){
a++;
alert(a);
}
}
var atest = new test( ); //引用返回的函数
atest( ); // 1
atest( ); // 2
上面的代码有两个主要的地方:
1、函数嵌套函数;
2、函数test返回内部匿名函数
根据执行结果可以发现,变量a一直保存在内存中,所有才会每调用一次atest(),返回的结果会自加1;究其原因,当执行到var atest = new test()时,函数test开始执行,完事之后,准备释放一波内存,结果返现其中返回的匿名函数应用了变量a,所以test()并不会出栈。想要更加透彻的理解这个过程,需要对js函数的作用域链比较熟悉。
二、闭包的原理
如果要更加深入的了解闭包以及函数a和嵌套函数b的关系,我们需要引入另外几个概念:函数的执行环境(excution context)、活动对象(call object)、作用域(scope)、作用域链(scope chain)。以函数a从定义到执行的过程为例阐述这几个概念。
1、当定义函数test的时候,js解释器会将函数test的作用域链(scopechain)设置为定义test时test所在的“环境”,如果test是一个全局函数,则scope chain中只有window对象。
2、当函数test执行的时候,test会进入相应的执行环境(excutioncontext)。
3、在创建执行环境的过程中,首先会为test添加一个scope属性,即test的作用域,其值就为第1步中的scope chain。即test.scope=test的作用域链。
4、然后执行环境会创建一个活动对象(call object)。活动对象也是一个拥有属性的对象,但它不具有原型而且不能通过JavaScript代码直接访问。创建完活动对象后,把活动对象添加到test的作用域链的最顶端。此时test的作用域链包含了两个对象:test的活动对象和window对象。
5、下一步是在活动对象上添加一个arguments属性,它保存着调用函数test时所传递的参数。
6、最后把所有函数test的形参和内部的函数(暂且命名为b)的引用也添加到test的活动对象上。在这一步中,完成了函数b的的定义,因此如同第3步,函数b的作用域链被设置为b所被定义的环境,即test的作用域。
到此,整个函数test从定义到执行的步骤就完成了。此时test返回函数b的引用给atest,又函数b的作用域链包含了对函数test的活动对象的引用,也就是说b可以访问到test中定义的所有变量和函数。函数b被atest引用,函数b又依赖函数test,因此函数test在返回后不会被GC回收。
当函数b执行的时候亦会像以上步骤一样。因此,执行时b的作用域链包含了3个对象:b的活动对象、test的活动对象和window对象,如下图所示:
如图所示,当在函数b中访问一个变量的时候,搜索顺序是先搜索自身的活动对象,如果存在则返回,如果不存在将继续搜索函数test的活动对象,依次查找,直到找到为止。如果整个作用域链上都无法找到,则返回undefined。如果函数b存在prototype原型对象,则在查找完自身的活动对象后先查找自身的原型对象,再继续查找。这就是Javascript中的变量查找机制。
三、闭包的应用
看一个题目:针对下面这个ul,编写js实现点击每一列的时候alert其index
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script>
window.onload = function init(){
var lis=document.querySelectorAll("#text li");
for(var i=0;i<lis.length;i++){
lis[i].onclick=function(){
alert(i);
}
}
}
</script>
</head>
<body>
<ul id="text">
<li>这是第一个li</li>
<li>这是第二个li</li>
<li>这是第三个li</li>
</ul>
</body>
</html>现在要求点击每个li输出的是对应的序号而不是全是3.
1、首先不使用闭包来做:
2、使用闭包来实现:
window.onload = function init(){
var lis=document.querySelectorAll("#text li");
for(var i=0;i<lis.length;i++){
lis[i].onclick=function(event){
//获取事件源对象
var src=event.srcElement||event.target();
//遍历lis数组
for(var n=0;n<lis.length;n++){
//判断数组中那个元素等于src
if(lis[n]==src){
//输入当前下标
alert(n);
}
}
}
}
}
实现思路
window.onload = function init(){
var lis=document.querySelectorAll("#text li");
for(var i=0;i<lis.length;i++){
lis[i].onclick=(function(e){
return function(){
alert(e);
};
})(i)
}
}
1.首先在遍历数组进行绑定onclick事件的时候,把当前的i传入事件函数中,并保存下来了。
2.把换行去掉后,在看看当前绑定的函数lis .onclick=(function(e){return function(){alert(i);};})(i)
3.当点击对应li元素的时候,事件函数返回一个function(){alert(i);}这样的方法。
4.但因为外层的function是一个自调函数,所以实际上应该是这样的(function(){alert(i);})(i)
5.所以输出的结果就是对应li数组中的下标。