刚进入JavaScript学习的时候,接触的概念中比较难以理解的就是闭包(closure)。网上和书上笼统的定义——所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

今天结合《JavaScript权威指南》中关于JavaScript变量的作用域的讲解、网上一些博客和资料以及工作中实际运用再来深入理解一下闭包的概念。

JavaScript的变量的作用域其实就是两种:全局变量(global)的作用域和局部变量(local)的作用域。

深入理解以下变量的作用域:JavaScript的解释器每开始执行一个函数时,都会为那个函数创建一个执行环境;每个JavaScript执行环境都有一个关联的作用域链,这个作用域链(chain scope)是一个对象列表或者对象链。当JavaScript代码需要开始获得变量的值时,它就开始沿着这个作用域链开始查找对象,如果查到了就采用该属性的值,否则继续沿着对象链查找。

先看一个简单的例子:

var x=1;

 function f(){
   var y=2;

   function g(){
     var z=3;
   }
 }

这一段代码的作用域链和变量查找的解析过程如下图所示:

再用网上有一道很著名的JavaScript的笔试题的demo讲解一下:

var a = 1;
  function f(){
      var a;
      alert(a);
  }
  alert(a);    //运行结果是1;
  f();         //运行结果是"undefined";

按照变量查找的解析过程来分析一下为什么f()的运行结果是”undefined”(f()的解析路径是红色描边的路径):

从以上的讲解也可以很清楚的知道:函数内部可以访问到函数外部的变量,但是函数外部的变量没办法访问到函数内部的变量。

由于函数外部没有办法访问到函数内部的变量,这时候我们要想一个办法——闭包就这样出现了。

function f(){
    var n = 1;
  }
  alert(n);  //代码运行结果是error, "n"未定义

但是函数内部可以访问函数外部的变量,所以我们在f()的函数体内再声明一个函数g(),这样函数g()可以访问到n,我们再令g作为f函数的返回值,我们就可以在f函数外访问到变量n了!

function f(){
    var n=1;

    function g(){
      alert(n);
    }

    return g();

 }

 f();  //运行结果为1

这个就是个简单的闭包了,所以在我看来闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成”一个定义在父函数内部并且能够访问到父函数内部的子函数或者表达式”,在这个例子中就是g()。

所以,通俗的来说,闭包就是将函数内部和函数外部连接起来的一座桥梁。这样理解起来就会简单一些。

闭包运用的场景:

  1. (1)保护函数内的变量安全。如a中的i只有b可以访问,不存在别的访问途径;
  2. (2)内存中维持一个变量。

下面我写了两个小DEMO,这些小DEMO也是工作中闭包常用场景的缩略图,我觉得这样可以帮助大家更好地理解闭包。

(1)函数feedCat中宠物的名字可以访问,但是不存在任何函数外可以设置的入口,宠物的名字被保护起来了,只允许通过闭包的方式获得宠物的名字。

function feedCat(people){
      var name = "duck";
      var nameState = {
          "girl" : "cat",
          "boy" : "dog",
          "grandpa" : "bird",
          "grandma" : "pig"
      };
      function setName(){
         name = nameState[people] ? nameState[people] : name;
     }
     setName();

     function getName(){
         alert(name);
     };

     return getName();
 }
 var cat0 = feedCat();       //"duck"
 var cat1 = feedCat("boy");  //"dog"
 var cat2 = feedCat("girl");  //"cat

上面的代码DEMO可以看出cat0、cat1和cat2这三个cat其实是三个独立的小实例,这三只宠物是互相独立的,互不影响的。正因为如此,在函数式风格的代码设计中,会大量运用到闭包来实现对一些变量或方法的封装。

(2)第二个小DEMO参考了网上曾经看过的一篇博客,里面有一个DEMO可以比较清晰的说明闭包的变量始终保持在内存中,我在此就拿这个DEMO来和大家说明下:

function f1(){

    var n=999;

    nAdd=function(){n+=1}

    function f2(){
      alert(n);
    }

   return f2;
 }

 var result=f1();
 result(); // 999
 nAdd();
 result(); // 1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是”nAdd=function(){n+=1}”这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

以上就是我在学习和研究闭包这个概念时总结的一点心得。欢迎大家一起探讨和研究哈。