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

来源:互联网 发布:远程狙击软件 编辑:程序博客网 时间:2024/04/29 04:04

表现

感觉好像打破了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>

完~

原创粉丝点击