您的位置:首页 > Web前端 > JavaScript

【js基础】怎么理解javascript中的闭包?

2017-08-09 01:00 651 查看

表现

感觉好像打破了javascript的作用域的约束,外层的作用域可以访问里面作用域的变量;

function foo(){
var a=5;
function baz(){
console.log(a);
}
return baz;
}
var f=foo();
f();//5


显然,变量f是在全局作用域中,a变量在foo函数的作用域中,但是在全局作用域中却访问并输出了a的值。

分析得,执行完foo()函数时,使f指向函数baz,接着执行f()语句时,即执行函数baz(),而baz()函数得作用域中包含函数foo()得作用域,还包含全局变量得作用域,所以可以访问到a的值。

所以我称为表现,其实本质上还是,里面的作用域访问外层的作用域中的变量。

本质

上述的例子是为了展示闭包,人为地修饰代码结构,现在,我们可以看一下二个经典的闭包例子。例一取自《javascript高级程序设计》第三版,例二取自《你不知道的javascrit》上卷。原理都一样。

例一:

function createFunctions(){
var result=new Array();
for(var i=0;i<10;i++){
result[i]=function(){
return i;
};
}
return result;
}

var r=createFunctions();
for(var j=0;j<r.length;j++){
console.log(r[j]());
}


如果我们不认真看一眼,十分容易得出 打印 0 1 2 3 4 5 6 7 9的结论。

其实是打印10个10,为什么会这样呢?下面我从内存的角度分析一下:

当执行var r=createFunctions();这条语句时,会调用createFunctions()函数,当执行for循环例里面的语句时,首先会使result[i]指向匿名函数,

当for结束后,i此时的值为10,而此时result是一个数组,保存着10个指向匿名函数的指针(本质上就是指针),虽然这十个指针都指向同一个匿名函数,但其实指向是不同的。可以讲10个result[i]分别指向10个大小相同的不同堆内存块中,而这十个对内存块中却引用同一个值i。

用图表示如下:



返回的result里面包含十个指针,分别指向不同内存块,返回时,在执行ri,时,即执行匿名函数,匿名函数会沿着作用域找i的值,在createFunctions(),函数中找到i的值,即for循环执行完后的值,所以i=10;所以最后输出10个10。

这个和c语言中让十个指针指向同一块内存,再让其中一个指针改变一下这块内存中的内容,然后在分别输出10个指针指向的内容差不多,肯定都输出改变后的内容。只不过这里内容还要沿着作用域链找一下而已。

那么问题来了,怎么可以让其输出 0-9呢?

最简单的方法,我们使用result=(function(){})() 称为立即执行函数(IIFE),直接返回值给每个result[i],而不让他为指针,直接为值(虽然指针和值本质上也是一样的)。如下:

function createFunctions(){
var result=new Array();
for(var i=0;i<10;i++){
result[i]=(function(){
return i;
})();//注意这里
// result[i]=new Function("i","return i");
}
return result;
}

var r=createFunctions();
for(var j=0;j<r.length;j++){
console.log(r[j]);//注意这里
}


第二种方法:考虑到出现10个10 的本质,是因为在执行r [i] ()时,会调用匿名函数,然后沿着作用域链,在createFunctions()函数中找到i,由于for循环,i已经自增到10,所以打印10个10。通过上面分析,我们直到直到沿着作用域链找到createFunctions()的作用域里,就无法改变输出10个10 的结果。所以我们就想能不能在沿着作用域链找到createFunctions()函数的作用域之前就找到i的值呢?如下:

function createFunctions(){
var result=new Array();
for(var i=0;i<10;i++){

result[i]=function(num){
return function(){
return num;     //注意这里
};
}(i);

}
return result;
}

var r=createFunctions();
for(var j=0;j<r.length;j++){
console.log(r[j]());
}


这里result返回数组同样包含十个指针,分别指向function(){return num;}在执行ri时,也会沿着作用域链向上找到第一个匿名函数,且时立即执行函数(IIFE),所以每次执行result[i]=function(num){}(i)时;会给num赋相应的值,所以在第一个匿名函数中就找到了num的值,即为每次传进来的i的值。

这里用可以表示为:



例二:

for(var i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i);
},i*1000);
}


通过上面的分析,这次我们小心翼翼分析一下:当i=1时,执行setTimeout(function timer(){console.log(i);},i*1000);表示1s后才执行function匿名函数,由于javascript是单线程的语言,所以执行i++,会比匿名函数执行快很多,所以等到i变为6时,至此,已经调用5次setTimeout在javascript的任务队列中等待执行,分别需要等1s,2s,3s,4s,5s才依次执行,当第一次执行匿名函数时,i已经6,所以沿着原型链找到全局作用域中的i=6,所以输出6,易得,后面全输出6,所以结果为5个6;

那么怎么才可以输出我们期望的 1 2 3 4 5 呢?

类比上面的分析,如果不想输出6,则一定不能让i的值在全局环境中得到。那我们就要想办法再加局部环境。再延伸至全局环境中提前找到。

for(var i=1;i<=5;i++){
(function(i){
setTimeout(function timer(){
console.log(i);
},i*1000);
})(i);
}


这样就行了,让i再第一个匿名函数的作用域中找到。注意写法。function外面的括号不能省略哦,否则就违反了js语法,若省略,在做语法分析时,会认为函数声明后面加了一个括号,这就会报错了。如果function前面再点东西,让编译器解释时,只要不认为是函数声明就行了,比如模仿jquery ,加一个+号。

应用

闭包的一个常见应用场景就是和循环在一起,比如网易云音乐的歌曲列表。



我们每点一首歌的歌名,对应的就获取列表的编号。

如果我们这样做可以吗?

var liList=document.querySelectorAll("li");
for(var i=0;i<liList.length;i++){
liList[i].onclick=function(){
alert(i);
};
}


我们来分析一下,页面加载完时,for循环已经执行完了,此时在我们的内存中,i=liList.length的值,liList类数组对象保存着liList.length个指针liList[i].onclick,都指向匿名函数(其实指向是不同的,上面已经详细说明了)。当某一个li上面触发click,会执行匿名函数,会执行alert(i),在匿名函数的作用域里没找到,就沿着作用域链找到全局环境,找到了i,而此时i=liList.length。所以不管我们点击哪一个li,都会alert的i的值都为liList.length。显然不是正确数据交互。

所以我们利用立即执行函数来解决这个问题。

var liList=document.querySelectorAll("li");
for(var i=0;i<liList.length;i++){
(function(num){
liList[i].onclick=function(){
alert(num);
};
})(i);
}


为此我写了一个样例:

测试:点这里

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<style>
html,body{
margin:0px;
padding:0px;
}
li{
list-style: none;
}
.main{
margin-top: 100px;
width:600px;
border: 1px solid red;
background-color:pink;
height: 200px;
}
.main>.lside,.main>.rside{
float:left;
}
.lside>ul>li,.rside>ul>li{
background-color: gray;
opacity: 0.8;
margin-top:5px;
width:200px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="main">
<div class="lside">
<ul>
<li>苏幕遮·碧云天</li>
<li>不能怕</li>
<li>九张机</li>
<li>水库</li>
<li>穿越火线</li>
</ul>
</div>
<div class="rside">
<ul>
<li>如果我爱你</li>
<li>老大</li>
<li>风去云不回</li>
<li>越过山丘</li>
<li>Super Tizzy</li>
</ul>
</div>
</div>
</body>

<script>
var liList=document.querySelectorAll("li");
for(var i=0;i<liList.length;i++){
(function(num){
liList[i].onclick=function(){
alert(num);
};
})(i);
}
</script>
</html>


完~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  闭包 javascript 函数