JavaScript闭包实例讲解

本篇博客不讲原理,只讲使用。(其实是博主不懂底层实现原理)。

在理解如何使用闭包之前,除了需要理解this以外,还需要分清变量中的引用传递,需要对如下代码的返回值很了解。
[js]
//字符串
var str="Hello Peanut";
function change_str($str){
$str="Hello Nutjs";
}
change_str(str);
alert(str);//Hello Peanut
//数字
var num=1024;
function change_num($num){
$num=2333;
}
change_num(num);
alert(num);//1024
//数组
var arr=[233];
function change_arr($arr){
$arr[0]++;
}
change_arr(arr);
alert(arr);//[234]
//对象
var obj={"pea":"nut"};
function change_obj($obj){
$obj["pea"]="rl";
}
change_obj(obj);
alert(obj["pea"]);//rl
[/js]

如果你对于上面的代码十分了解其原理,那么就可以继续往下看了。如何你完全不知道上面的代码为什么会输出那些值,那么可能你需要重新去温故一下ECMA的变量传递了。

下面开始正文:

一句话说闭包:子函数可以读取父函数的变量。

那么下面的代码是成立的:
[html]
<ul>
<li id="peanut">My name is Peanut.</li>
<li id="hello">Hello Peanut!</li>
</ul>

<script>
var elt_pea=document.getElementById("peanut");
elt_pea.onclick=function(){
alert(elt_pea.innerHTML);
};
</script>
[/html]
这段代码很好理解,给elt_pea元素绑定的onclick函数可以读取到它上一层定义的elt_pea变量。当然用this也是可以的
[html]
<script>
var elt_pea=document.getElementById("peanut");
elt_pea.onclick=function(){
alert(this.innerHTML);
};
</script>
[/html]

不过这里就和闭包没有关系的。

那么我们现在要实现一个功能,点击li元素的时候给它加一个编号,编号就是li元素出现的顺序。所以我们可能会考虑这么写
[html]
<ul>
<li id="peanut">My name is Peanut.</li>
<li id="hello">Hello Peanut!</li>
</ul>

<script>
var elts_list=document.getElementsByTagName("li");
for(var i=0;i<elts_list.length;i++){
elts_list[i].onclick=function(){
this.innerHTML=i+"."+this.innerHTML;
};
};
</script>
[/html]
看似逻辑没有问题,实际上却是不对的,点击任意一个li元素都会在前面添加序号2,不是0也不是1,而是2。

结合闭包的特性,子函数可以读取父函数的变量值,那么错误其实也不难理解。

当执行elt绑定的onclick函数的时候,其中的i已经经历了3次循环由0变为2了,那么这时候函数向上读取i变量自然就找到了2。请看下面的代码:
[html]
<script>
var num=0;
window.onclick=function(){
alert(num);
};
num=1;
</script>
[/html]
当点击窗口时,会弹出1而不是0。所以你哪怕把上面的代码改成这样,运行结果依旧是错的。
[html]
<ul>
<li id="peanut">My name is Peanut.</li>
<li id="hello">Hello Peanut!</li>
</ul>

<script>
var elts_list=document.getElementsByTagName("li");
var index=0;
elts_list[index].onclick=function(){
this.innerHTML=index+"."+this.innerHTML;
};
index++
elts_list[index].onclick=function(){
this.innerHTML=index+"."+this.innerHTML;
};
</script>
[/html]
除非你再新建一个变量才能解决问题
[html]
<ul>
<li id="peanut">My name is Peanut.</li>
<li id="hello">Hello Peanut!</li>
</ul>

<script>
var elts_list=document.getElementsByTagName("li");
var index=0;
elts_list[index].onclick=function(){
this.innerHTML=index+"."+this.innerHTML;
};
var other_index=1;
elts_list[other_index].onclick=function(){
this.innerHTML=other_index+"."+this.innerHTML;
};
</script>
[/html]

不过这样看起来很糟糕,那么我们有什么解决办法的?那就是利用this变量。

先说下我们的思路,我们可以让元素每次发现自己被点击的时候这样“啊啊,我被用户点击了,我要给自己加一个前缀数字,容我找一找自己在li标签里面排老几......啊,找到了,就是这个”。

那么代码如下:
[html]
<ul>
<li id="peanut">My name is Peanut.</li>
<li id="hello">Hello Peanut!</li>
</ul>

<script>
var elts_list=document.getElementsByTagName("li");
for(var i=0;i<elts_list.length;i++){
elts_list[i].onclick=function(){
//我自己在li标签的排行
var self_index=null;
//从所有的li标签中找自己
for(var v=0;v<elts_list.length;v++){
//如果有一个标签和我自己一样
if(elts_list[v] === this){
//记录自己的索引
self_index=v;
break;
}
}
//添加自己的索引
this.innerHTML=v+"."+this.innerHTML;
};
}
</script>
[/html]

这样程序执行的结果就是我们想要的了,不过略有些复杂,每次都要找一找自己排第几,感觉很怪异,元素难道不知道自己排第几吗?确实不知道,不过我们可以提前告诉它,那么元素被点击的思路应该是这样的“啊啊,我被点击了,我不知道自己排第几,不过曾经有人告诉过我,我就不自己再找了。”

实现代码如下:
[html]
<ul>
<li id="peanut">My name is Peanut.</li>
<li id="hello">Hello Peanut!</li>
</ul>

<script>
var elts_list=document.getElementsByTagName("li");
for(var i=0;i<elts_list.length;i++){
//告诉元素你排第几
elts_list[i].selfIndex=i;
elts_list[i].onclick=function(){
this.innerHTML = this.selfIndex+"."+this.innerHTML;
};
}
</script>
[/html]

这样也能实现我们想要的效果,而且比之前的方法简洁高效很多。而实际应用中,我们大部分都是这样来避免闭包带给我们的弊端的。

闭包虽然强大,但是其弊端也显而易见,我们需要巧妙的使用this来避免闭包带给我们的问题。

但是有时候却不那么如意,有些情况你甚至连this都用不了,请确保你完全理解上面的例子,下面我们将讨论一下this用不了的情况setInterval和setTimeout方法。

这次依旧要实现上面的功能,不过是点击后延迟300毫秒再加前导数字。

如果直接这么写,那么程序将不会有任何反应:
[html]
<ul>
<li id="peanut">My name is Peanut.</li>
<li id="hello">Hello Peanut!</li>
</ul>

<script>
var elts_list=document.getElementsByTagName("li");
for(var i=0;i<elts_list.length;i++){
//告诉元素你排第几
elts_list[i].selfIndex=i;
elts_list[i].onclick=function(){
setTimeout(function(){
this.innerHTML = this.selfIndex+"."+this.innerHTML;
},300);
};
}
</script>
[/html]
如果你尝试在setTimeout函数中打印出this,程序会告诉你是[object Window],那么我们可以利用闭包的特性让里面的函数获取到外面函数的this
[html]
<ul>
<li id="peanut">My name is Peanut.</li>
<li id="hello">Hello Peanut!</li>
</ul>

<script>
var elts_list=document.getElementsByTagName("li");
for(var i=0;i<elts_list.length;i++){
//告诉元素你排第几
elts_list[i].selfIndex=i;
elts_list[i].onclick=function(){
var that=this;
setTimeout(function(){
that.innerHTML = that.selfIndex+"."+that.innerHTML;
},300);
};
}
</script>
[/html]

这样程序就运行对了。

但是有时候可能你根本没办法获取到外层的this,或者根本就没有this可以获取。比如下面的这个需求:在执行脚本300秒后自动添加前导数字。那么实现代码应该怎么写?难道要这样?
[html]
<ul>
<li id="peanut">My name is Peanut.</li>
<li id="hello">Hello Peanut!</li>
</ul>

<script>
var elts_list=document.getElementsByTagName("li");
for(var i=0;i<elts_list.length;i++){
//告诉元素你排第几
elts_list[i].selfIndex=i;
setTimeout(function(){
elts_list[i].innerHTML = elts_list[i].selfIndex+"."+elts_list[i].innerHTML;
},300);
}
</script>
[/html]
运行代码后,编译器会直接提示错误。很明显,我们犯了一开始的错误,i变量在执行完倒计时的时候已经变成了2了,读取elts_list[2]自然会报错,因为只有2个li元素。

闭包读取变量有问题,而this又根本用不了,好像根本就是无解。

不过通过我们阅读参考手册发现一个突破点,那就是setInterval和setTimeout方法第一个参数不仅仅可以接收一个函数,还可以接收一个字符串。

那么问题就解决了:

[html]
<ul>
<li id="peanut">My name is Peanut.</li>
<li id="hello">Hello Peanut!</li>
</ul>

<script>
var elts_list=document.getElementsByTagName("li");
for(var i=0;i<elts_list.length;i++){
//告诉元素你排第几
elts_list[i].selfIndex=i;
setTimeout(
'elts_list['+i+'].innerHTML = elts_list['+i+'].selfIndex+"."+elts_list['+i+'].innerHTML;'
,300);
}
</script>
[/html]
这里我们完全没有使用this,而且因为直接是拼接字符串,所以闭包读取的变量不会产生问题。因为是拼接字符串,代码还可以直接这样
[html]
<ul>
<li id="peanut">My name is Peanut.</li>
<li id="hello">Hello Peanut!</li>
</ul>

<script>
var elts_list=document.getElementsByTagName("li");
for(var i=0;i<elts_list.length;i++){
setTimeout(
'elts_list['+i+'].innerHTML = "'+i+'."+elts_list['+i+'].innerHTML;'
,300);
}
</script>
[/html]

到现在。虽然我们依旧不了解闭包的实现原理,但是闭包带来的问题我们却全部解决了。

发表新的回复