代码改变世界

javascript 闭包的理解(一)

2014-12-20 20:52  盛世游侠  阅读(1692)  评论(4编辑  收藏  举报

过很多谈如何理解闭包的方法,但大多数文章,都是照抄或者解释《Javascript高级程序设计(第三版)》对于闭包的讲解,甚至例程都不约而同的引用高程三181页‘闭包与变量’一节的那个“返回数组各个项,结果各个项的值都相同”的例程,还有些文章的讲解过程上一步与下一步之间的跨度简直就是一步登天,让人反复看半天都无法理解。

闭包的理解需要很多概念做铺垫,包括变量作用域链、执行环境、变量活动对象、引用式垃圾内存收集机制等,如果对本文涉及的这些概念不理解,可以去找本《Javascript高级程序设计(第三版)》好好看看并理解这些概念。

 

我自己也是在反复编写一些例子并仔细认真的阅读和理解高程三与闭包相关的概念和知识后,终于对闭包有了一定的理解,为了检验学习效果,特别写篇文章以便检验自己是否真正理解和掌握。

 

在理解闭包之前,我们先来看些理解闭包容易忽略的小东西。

//例子1:
function foo(){
    var a = 1;
    return a;    
};
console.log(foo()); //1

上面的这个例子很好理解,一个函数如果return一个值,那么这个函数就可以当作return的值直接使用,无论它return的是一个引用类型(函数、数组)还是基本数据类型。

 

//例子2:
function foo(){

    function foos(){
        var b = 2;
        return b;        
    }
    
};
console.log(foos()); //Uncaught ReferenceError: foos is not defined

这个例子说明,如果一个函数B被声明在一个函数A的内部,那么在函数A外部,通过函数B的函数名直接调用函数B,是会出未定义错误的。你可以理解为:变量作用域规则中的局部作用域规则对于函数声明同样成立。

 

//例子3:
function foo(){
    
    var a = 1;
    
    function foos(){
        return a;//注意:返回的a是foo中定义的变量,而不是foos中定义的变量。
    }
    
    console.log(foos()); 
};

foo();//1

例子3说明foos只能在foo内部执行,这符合我们对于“变量作用域规则中的局部作用域规则,对于函数声明同样成立”的理解。

 

//例子4:
function
foo(){ var arr = new Array();
for(var i=0;i<10;i++){ arr[i] = function(){ return i; }; } return function(){ for(var k = 0;k<arr.length;k++){ console.log(arr[k]()); } }; }; console.log(foo()());//输出10个10;

例子4中中执行foo()以后得到的是foo自己返回的一个匿名函数(如下所示):

return function(){
        for(var k = 0;k<arr.length;k++){
            console.log(arr[k]());
        }    
    };

那么,此时我们还需要执行一遍这个得到的匿名函数,才能得出最后的值(也就是:10个10),而执行这个得到的匿名函数(我们不妨叫它函数C)时,foo已经执行了一遍了,这个时候foo的变量i的值已经是10了,由于变量对象是通过引用赋值的,所以这个时候foo内的闭包函数(也就是引用了foo的变量i的匿名函数)再来引用foo的变量i,那么得到的值自然就是10了。这里的关键在与foo内的闭包函数执行的时机,如果闭包函数立即执行后再赋值给arr[i],那么就可以得到我们期望的值0到9了;

//例子5:
function
foo(){ var arr = new Array(); for(var i=0;i<10;i++){ arr[i] = function(){ return i; }(); } return function(){ for(var k = 0;k<arr.length;k++){ console.log(arr[k]); } }; }; console.log(foo()());//0到9

 

关于例子4更详细的解释:
在例子4中,根据javascript的引用垃圾收集机制(这个机制的规则是:存在大于0个引用的变量不应该被销毁),虽然此时函数foo已经执行完了,但是foo的这个变量i因为被匿名函数(我们不妨叫它函数B)引用,所以这个i还是没有被垃圾收集程序销毁,那么这个i此时存在哪里呢?肯定不是存在于函数foo的执行环境里面,因为执行环境创建和销毁的规则是:当一个函数创建时,则创建这个函数的执行环境,一旦这个函数执行完毕则马上销毁这个执行环境。因此这个i只可能存在于foo内部的匿名函数(函数B)的执行环境中,这个函数B的执行环境中不但有自己的作用域和自己的变量对象,而且包含了其外部函数foo的作用域和foo的变量对象,变量i就存在于函数B包含的foo的作用域中和变量对象中。由于javascript不能直接操作内存空间,所以javascript只能以引用的方式访问
变量对象,当foo的变量i的值已经变成10了的时候,再去执行foo的闭包函数(函数B),那么此时对i的引用自然会得到10。

 

使用以上实验得出的原理,我们还可以很容易的理解《Javascript高级程序设计(第三版)》181页最下面的那个例子:

//《Javascript高级程序设计(第三版)》181页最下面的那个例子:


function createFunctions(){
    var result = new Array();
    
    for(var i=0;i<10;i++){
        result[i] = function(num){
            return function(){
                return num;    
            }    
        }(i);//这里是将参数传给该匿名函数,并且立即执行该匿名函数的写法;
    }
    return result;
}

for(var i=0;i<10;i++){
    console.log(createFunctions()[i]());
}//0,1,2,3,4,5,6,7,8,9

 

同时,我们可以很容易的改写这个例子:

function createFunctions(){
    var result = new Array();
    
    for(var i=0;i<10;i++){
        result[i] = function(){
            return function(){
                return i;    
            }()//这里添加了立即执行
        }();//这里是将参数传给该匿名函数,并且立即执行该匿名函数的写法;
    }
    return result;
}

for(var i=0;i<10;i++){
    console.log(createFunctions()[i]);//这里去掉了一对括号
}//0,1,2,3,4,5,6,7,8,9

 

这个改写的例子同时证明,最内层的匿名函数还是可以引用最外面全局函数的变量。

 

 

下面这张图片中的例子,使用闭包就可以很好的理解了:

 

要是实在不理解,还可以试着这样来验证一下帮助理解:

 

function foo(){

   var diss = "mem_count";

   return function(){
       
       return diss;

   }

}


var fo = foo();

var as = fo();
//undefined
as
//"mem_count"
var as2 = fo();
//undefined
as2
//"mem_count"
fo()
//"mem_count"
fo()
//"mem_count"
fo()
//"mem_count"

 

如果这样还是没办法理解,那真是应该先去看看变量的作用域、垃圾回收机制,函数表达式等基础知识了。