JavaScript闭包实例讲解

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

在理解如何使用闭包之前,除了需要理解this以外,还需要分清变量中的引用传递,需要对如下代码的返回值很了解。

//字符串
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

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

下面开始正文:

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

那么下面的代码是成立的:

<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>

这段代码很好理解,给elt_pea元素绑定的onclick函数可以读取到它上一层定义的elt_pea变量。当然用this也是可以的

<script>
var elt_pea=document.getElementById("peanut");
elt_pea.onclick=function(){
    alert(this.innerHTML);
};
</script>

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

那么我们现在要实现一个功能,点击li元素的时候给它加一个编号,编号就是li元素出现的顺序。所以我们可能会考虑这么写

<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>

看似逻辑没有问题,实际上却是不对的,点击任意一个li元素都会在前面添加序号2,不是0也不是1,而是2。

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

当执行elt绑定的onclick函数的时候,其中的i已经经历了3次循环由0变为2了,那么这时候函数向上读取i变量自然就找到了2。请看下面的代码:

<script>
var num=0;
window.onclick=function(){
    alert(num);
};
num=1;
</script>

当点击窗口时,会弹出1而不是0。所以你哪怕把上面的代码改成这样,运行结果依旧是错的。

<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>

除非你再新建一个变量才能解决问题

<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>

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

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

那么代码如下:

<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>

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

实现代码如下:

<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>

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

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

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

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

如果直接这么写,那么程序将不会有任何反应:

<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>

如果你尝试在setTimeout函数中打印出this,程序会告诉你是[object Window],那么我们可以利用闭包的特性让里面的函数获取到外面函数的this

<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>

这样程序就运行对了。

但是有时候可能你根本没办法获取到外层的this,或者根本就没有this可以获取。比如下面的这个需求:在执行脚本300秒后自动添加前导数字。那么实现代码应该怎么写?难道要这样?

<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>

运行代码后,编译器会直接提示错误。很明显,我们犯了一开始的错误,i变量在执行完倒计时的时候已经变成了2了,读取elts_list[2]自然会报错,因为只有2个li元素。

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

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

那么问题就解决了:

<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>

这里我们完全没有使用this,而且因为直接是拼接字符串,所以闭包读取的变量不会产生问题。因为是拼接字符串,代码还可以直接这样

<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>

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

发表新的回复