深入理解JS闭包

 在阅读这篇文章之前,我们先来做做两个思考题,有助于我们深入理解闭包,这也是我今天发现的比较有意思的题目,一起来看看吧:

var n = 1;
function fun1() {
   test = 10;    
   var n = 999;    
   nAdd = function() {
         n += 1;
         console.log(n);
   }    
   function fun2() {      
        console.log(n);    
   }    
   return fun2;  
}
var result = fun1();
result(); // 999
console.log(test);//10
console.log(n);//1
nAdd();
result(); // 1000

由于在函数内部声明变量test和nAdd的时候没有使用var命令,在函数fun1执行的时候这两个变量便作为全局变量来声明

我见网上有博主说闭包就是嵌套函数中的内部函数,也有博主说闭包是嵌套函数的外部函数,从上面这个栗子来看,显然这都是不准确的说法,在这里官方给出的定义更为准确,我们先来看看官方给出的定义吧:

闭包是函数和声明该函数的词法环境的组合。

再来看看官方给出的示例:

function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name);
    }
    return displayName;
}

var myFunc = makeFunc();
myFunc();

很显然这里存在嵌套函数,并且嵌套的内部函数使用了外部函数的变量,于是产生了闭包,按照官方的定义来理解的话,这里的闭包指的就是从makeFunc函数的第一句到return语句这一句。

感觉看了上面这一堆还是稀里糊涂怎么办?来,我们再重新捋一捋。

一、理解闭包第一步,理解JS变量的作用域 

要真正理解闭包,首先,我们必须理解Javascript特殊的变量作用域(具体可以参考我的上篇文章,深入理解JS作用域和作用域链),JavaScript的一个特殊之处就在于它的变量访问规则:

1.函数内部可以直接读取全局变量

  var n=999;

  function f1(){
    alert(n);
  }

  f1(); // 999

2.在函数外部无法读取函数内的局部变量

  function f1(){
    var n=999;
  }

  alert(n); // Uncaught ReferenceError: n is not defined

但是这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量! (补充:在函数内部声明全局变量被称为隐式声明)

  function f1(){
    n=999;
  }
  f1();
  alert(n); // 999

3.内部函数可以访问外部函数的变量

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

二、理解闭包第二步,如何实现从外部读取局部变量

在使用JS进行编程的时候,我们有时候需要在函数外部得到函数内的局部变量,可是我们前面已经说了,正常情况下这是不可能的。嘿嘿,既然都说了是正常情况,那肯定有“不正常情况”啦!那就是在函数的内部,再定义一个函数。

  function fun1(){
    var n=999;
    function fun2(){
      alert(n); // 999
    }
  }
    fun1();

在上面的代码中,函数fun2就被包括在函数fun1内部,这时fun1内部的所有局部变量,对fun2都是可见的。但是反过来就不行,fun2内部的局部变量,对fun1都是不可见的。这就是Javascript语言所特有的链式作用域结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。总结:父对象的所有变量,对子对象都是可见的,反之则不成立。

这样我们就能够访问到函数fun1内部的变量了,可是问题来了,我们无法从外部调用函数fun2,那我们应该怎样才能够在外部访问fun2呢,我们只需要将fun2作为fun1返回值返回,这样我们不就能在fun1外部访问到fun1内部的变量了吗,如下:

  function fun1(){
    var n=999;
    function fun2(){
      alert(n);
    }
    return fun2;
  }
  var result=fun1();
  result(); // 999

三、理解闭包第三步,闭包的概念

关于闭包的概念,在前面我们已经说了官方给出的定义,官方给的定义虽然比较准确一点,但是对于初学者来说晦涩难懂,这里来说说我对闭包的理解,我的理解是,闭包就是能够读取其他函数内部变量的函数。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁

前面我们虽然也说了闭包不是一个函数,但是为了方便理解和学习我们通常可以将闭包称为一个函数,但是我们也要时刻在心里记住闭包不是一个函数,而是函数和声明该函数的词法环境的组合,这个环境包含了这个闭包创建时所能访问的所有局部变量。记住,它是一个组合!组合!

四、理解闭包的第四步,闭包的用途

想要使用闭包,必须知道它的结构,也是它的产生条件

  1. 一个函数,里面有一些变量和另一个函数
  2. 外部函数里面的函数使用了外部函数的变量
  3. 外部函数最后把它里面的那个函数用return抛出去

以及闭包的作用

  • 在函数外部可以读取函数内部的变量
  • 让这些变量的值始终保持在内存中

现在我们再来分析分析文章开始给出的那个题目,为了方便观察,我在这里再插入那段代码:

var n = 1;
function fun1() {
   test = 10;    
   var n = 999;    
   nAdd = function() {
         n += 1;
         console.log(n);
   }    
   function fun2() {      
        console.log(n);    
   }    
   return fun2;  
}
var result = fun1();
result(); // 999
console.log(test);//10
console.log(n);//1
nAdd();
result(); // 1000

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

发生这样的情况原因就在于fun1是fun2的父函数,而fun2又通过fun1的return语句被赋给了一个全局变量,这导致fun2始终在内存中,而fun2的存在依赖于fun1,因此fun1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

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

使用闭包时需要注意:

  • 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,可能导致内存泄露。解决方法是,在退出函数之前,将不需要的局部变量赋值为null。
  • 闭包会在父函数外部改变父函数内部变量的值。如果你把父函数当作对象使用,把闭包当作它的公用方法,把内部变量当作它的私有属性,这时一定要注意,不要随便改变父函数内部变量的值。
  • 当定义一个函数时,它实际上保存一个作用域链。当调用这个函数时,它创建一个新的对象来存储它的局部变量,并将这个对象添加至保存的那个作用域链上,同时创建一个新的更长的表示函数调用作用域的“链”。对于嵌套函数来讲,事情变得更加有趣,每次调用外部函数时,内部函数又会重新定义一遍。因为每次调用外部函数的时候,作用域链都是不同的。内部函数在每次定义的时候都有微妙的差别——在每次调用外部函数时,内部函数的代码都是相同的,但是关联这段代码的作用域链不相同,用闭包概念来说,也就是产生了新的闭包

关于闭包的理解,到这里就结束了,希望对大家深入理解有帮助,要是有什么不理解也欢迎评论区留言,笔者会一一答复。大家也不要害怕闭包,闭包虽然难,可是闭包真的很重要,非常值得学习,虽然闭包在开发的时候用的很少,但是用到的时候几乎都是不可被别的方式替代的,但凡遇到永久 ,保护等关键字就是用闭包。