首页 > 代码库 > 作用域、执行环境、闭包(四)

作用域、执行环境、闭包(四)

本文也同步发表在我的公众号“我的天空

 

技术分享

 

上一期我们已经介绍了闭包,由于闭包可以延长函数内部的变量的生存周期,因此我们可以将不需要暴露在全局的变量封装成函数的内部变量,从而避免代码污染。

 

譬如要实现一个简单的累加器,为了保存每次累加的结果,因此声明了一个全局变量total,代码如下:

 

var total=0;
function add(t){
    total+=t;
    alert(total);
}
total=2;
add(3);        //显示5
add(5);        //显示10
add(1);        //显示11

 

但是在实际开发中,应尽量避免全局变量,因为全局变量可以在代码的任何地方被调用,假设在其他地方,不小心更改了total的值的话,我们这个累加器就会出问题了。由于total值是只供函数add()使用的,因此希望total能被封闭在函数add()的内部,这样,就无法从外部来改写它了,我们使用闭包来实现,代码如下:

 

function add(s){
    var total=s;
    return function(t){
        total+=t;
        alert(total);
    }
}
var a=add(2);
a(3);        //显示5
a(5);        //显示10
a(1);        //显示11

 

通过闭包,将变量total封闭在函数add()中,外部无法访问到,这样就避免了被其他代码随意改写的可能性。

 

由于闭包会将封闭在函数内部的局部变量赋予类似于全局变量的效果,因此在有些场景下需要特别注意,尤其是涉及到循环遍历,来看以下代码:

 

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

 

这是一个创建数组数组的函数,从表面上看,每个函数应该都返回自己的索引值,因此创建的数组中,每个元素应该包含如下函数:

 

result[0]:function(){return 0}
result[1]:function(){return 1}
result[2]:function(){return 2}

 

但是实际上,数组中的每个元素只是包含:function(){return i},也就是说,当该函数执行完毕后,返回的是这样一个数组:

 

{
    function(){return i},
    function(){return i},
    function(){return i}
}

 

而变量i由于存在于一个返回函数中,形成了闭包,所以当createArray()执行完毕后,其执行环境不会被销毁,变量i得以保留,并且其值为3(这点很重要)。

 

因此,当我们使用createArray()来创建数组时,得到的效果就不是我们的预期,弹出的都为“3”:

 

var a=createArray();
for(var z=0;z<a.length;z++){
    alert(a[z]());    //均显示为3
}

 

实际上该代码无非就是重复执行三遍以下代码:

 

alert(function(){return 3}());


那么为了达到我们的预期,应该将createArray()函数做如下修改:

 

function createArray(){
    var result=new Array();
    for (var i=0;i<3;i++){
        result[i]=function(z){
            return function(){
                return z;
            };
        }(i)
    }
}

 

分析以上代码,我们将一个自执行函数返回给了数组元素,在赋值的时候,变量z就是在赋值的那个时刻的i值,那么返回的数组中的元素便包含我们预期的函数:

 

result[0]:function(){return 0}
result[1]:function(){return 1}
result[2]:function(){return 2}

 

再一次执行以下代码,显示就正常了:

 

var a=createArray();
for(var z=0;z<a.length;z++){
    alert(a[z]());    //依次显示0、1、2
}


一定要注意的是,我们是把一个函数赋予了数组中的元素,而不是单个的值。因为在实际的应用中,返回函数的话,我们就可以在函数内做更多的事情。

 

看以下的实现,html有四个p标签和4个div标签,当单击div标签时相应的p标签更改颜色,请注意这是一个面试中非常容易遇到的题目,代码如下:

 

<body>
  <p>p1</p><p>p2</p><p>p3</p><p>p4</p>
  <div>div1</div><div>div2</div><div>div3</div><div>div4</div>  
 </body>

  <script>
     var d=document.getElementsByTagName("div");
       for(var i=0;i<d.length;i++){
         d[i].onclick=function(num){
             return function(){
                document.getElementsByTagName("p")[num].style.color="red";
             };
         }(i);
       }
 </script>

 

如果直接写成以下代码的话,那么无论你单击哪个div,程序总是会报错,因为此时i的值为4,所以document.getElementsByTagName("p")[4]这个元素并不存在,导致引用错误。

 

   for(var i=0;i<d.length;i++){
        d[i].onclick=function(){
          document.getElementsByTagName("p")[i].style.color="red";
      };
   }

 

最后我们要注意的是当需要返回函数内部的多个变量时,便不能采用返回匿名函数的方式了,可以采用以下的形式:

 

function setpepole(){
    var name="李四";    
    var age=31;
    return {
        getname:funcion(){
            return name;    
        },
        getage:function(){
            return age;
        }
    }
}
var a=setpepole();
alert(a.getname());     //显示“李四”
alert(a.getage());      //显示31

 

闭包系列就到此全部结束了!

 

作用域、执行环境、闭包(四)