JavaScript中的闭包理解

原创文章,转载请注明:JavaScript中的闭包理解  By Lucio.Yang

1.JavaScript闭包

  在小学期开发项目的时候,用node.js开发了服务器,过程中遇到了node.js的第一个陷阱:由事件和回调函数形成的特殊的循环。解决这个问题时我使用了创建闭包的方法,当然如果不需要控制循环的变量的话也可以使用数组的forEach函数。最近ES6在紧锣密鼓的准备,新标准里面的Harmony Generator和yield十分引人瞩目,也可以用来决解这个问题。这是后话了。

  这里来着重介绍一下js的闭包这一重要特性。

  首先什么是闭包?

  1.1闭包的概念解释

    闭包(wikipedia):In programming languages, a closure (also lexical closure or function closure) is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.

    也就是说,闭包是一个函数或者函数的引用,并绑定了一些变量和其生存环境。

    通过以下闭包的两个简单的用途来看闭包的概念。

    1.提取函数内部的变量

function f1(){
  n=999;
  function f2(){
    alert(n);
  }
  return f2;
}
var result=f1();
result(); // 999

    函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1 就是不可见的。这就是Javascript语言特有的“链式作用域”结构(chain scope)。简单来说,作用域链就是函数在定义的时候创建的,用于寻找使用到的变量的值的一个索引,而他内部的规则是,把函数自身的本地变量放在最前面,把自身的父级函数中的变量放在其次,把再高一级函数中的变量放在更后面,以此类推直至全局对象为止。当函数中需要查询一个变量的值的时候,js解释器会去作用域链去查找,从最前面的本地变量中先找,如果没有找到对应的变量,则到下一级的链上找,一旦找到了变量,则不再继续。如果找到最后也没找到需要的变量,则解释器返回undefined。正是因为这种机制,闭包才能提取父级函数内部的变量。

    2.在内存中保有某些变量。

function f1(){
 var n=999;
 nAdd=function(){n+=1}
 function f2(){
  alert(n);
 }
 return f2;
}
var result=f1();
result(); // 999
nAdd();
result(); // 1000

    首先明确一点,在函数内部定义变量/函数的时候,如果不用var,那么该变量/函数就是全局变量/函数。

    result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。这里需要解释一下js的内存回收机制。

    一般来说,一个函数在执行开始的时候,会给其中定义的变量划分内存空间保存,以备后面的语句所用,等到函数执行完毕返回了,这些变量就被认为是无用的了。对应的内存空间也就被回收了。下次再执行此函数的时候,所有的变量又回到最初的状态,重新赋值使用。但是如果这个函数内部又嵌套了另一个函数,而这个函数是有可能在外部被调用到的。并且这个内部函数又使用了外部函数的某些变量的话。这种内存回收机制就会出现问题。如果在外部函数返回后,又直接调用了内部函数,那么内部函数就无法读取到他所需要的外部函数中变量的值了。所以js解释器在遇到函数定义的时候,会自动把函数和他可能使用的变量(包括本地变量和父级和祖先级函数的变量(自由变量))一起保存起来,也就是构建一个闭包,这些变量将不会被内存回收器所回收,只有当内部的函数不可能被调用以后(例如被删除了,或者没有了指针),才会销毁这个闭包,而没有任何一个闭包引用的变量才会被下一次内存回收启动时所回收。

    3.实现对象的封装

var person = function(){  
    //变量作用域为函数内部,外部无法访问  
    var name = "default";     
     
    return {  
       getName : function(){  
           return name;  
       },  
       setName : function(newName){  
           name = newName;  
       }  
    }  
}();  
   
print(person.name);//直接访问,结果为undefined  
print(person.getName());  
person.setName("abruzzi");  
print(person.getName());  
 
得到结果如下:
 
undefined
default
abruzzi

    这是一个简单的js的对象的封装,只有通过对象才可以访问对象中的变量,实现了变量和环境(对象)的绑定。也就是通过闭包,我们模拟了面向对象语言中的封装对象的模板。

    4.高效率的匿名自执行函数

var datamodel = {  
    table : [],  
    tree : {}  
};  
   
(function(dm){  
    for(var i = 0; i < dm.table.rows; i++){  
       var row = dm.table.rows[i];  
       for(var j = 0; j < row.cells; i++){  
           drawCell(i, j);  
       }  
    }  
     
    //build dm.tree    
})(datamodel); 

    代码中的闭包实际是用来做UI的初始化,执行一次后临时变量即销毁,这样的机制不会污染全局变量,效率很好。

  1.2闭包与匿名函数的概念辨别

    首先来看匿名函数的概念:

    anonymous function(wikipedia):In computer programming, an anonymous function (also function literal or lambda abstraction) is a function definition that is not bound to an identifier.

    简单来说,没有标识符的函数就是匿名函数。一下是匿名函数的一些写法:

(function() {})();最常见到的;
(function(){}());
void function(){};
错 误的写法
function(){}(); 

    关于匿名函数的原理和理解这里就不再赘述。我们只探讨匿名函数和闭包的关系。

    再回顾一下闭包的概念中的关键字-“ is a function or reference to a function”,说明只要是函数内部申明的函数是闭包,不管是什么函数。所以,得出结论:

      如果匿名函数不在函数内部,就不是闭包。

      函数内的匿名函数如果引用了父函数的变量,那么这个匿名函数就成为了一个闭包。

    就这么简单"( ̄▽ ̄)"""。所以产生了以下的闭包的定义方法:

var datamodel = {  
    table : [],  
    tree : {}  
};  
   
(function(dm){  
    //something
})(datamodel); 

 

参考:javascript深入理解闭包-脚本之家墓中无人sunlylorn十个流年

posted @ 2014-10-24 12:16  Lucio.Yang  阅读(355)  评论(0编辑  收藏  举报