对JavaScript中闭包的理解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_36276528/article/details/70049825

之前对于闭包这个概念的理解都是模糊的,只是单纯的知道闭包的作用:
1. 可以在函数的外部访问到函数内部的局部变量。
2. 让这些变量始终保存在内存中,不会随着函数的结束而自动销毁。

  而这几天通过各种资料和博客的学习,自认对闭包的概念和原理有了一定的了解,所以来分享一下我的心得。如果文中有什么不当之处,请多多谅解,并给与指正。谢谢!

什么是闭包?

在维基百科中的描述是:

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

上面的描述可能对于初学者来说有点晦涩。所以我们再来看看在JavaScript高级程序设计(第3版)中的描述:

闭包是指有权访问另一个函数作用域中的变量的函数。

  综合这两个解释,我们大概的可以理解为:闭包是可以在另一个函数的外部访问到其作用域中的变量的函数。而被访问的变量可以和函数一同存在。即使另一个函数已经运行结束,导致创建变量的环境销毁,也依然会存在,直到访问变量的那个函数被销毁。就如下面代码所示:

function add() {
    var sum = 0;
    function operation() {
        return sum = sum ? sum + 1 : 1;
    }
    return operation
}

var a = add();
console.log(a());//1
console.log(a());//2
console.log(a());//3
console.log(a());//4
a = null;
a = add();
console.log(a());//1

  那么为什么会这样了?我们都知道在javaScriptS中,在函数的外部是不能访问函数内部的变量的。并且随着函数运行的结束,函数内部定义的变量也会被JavaScript的自动回收机制回收掉,并不会一直存在。那为什么闭包可以不受限制了,下面我们就来详细讨论下这个问题。

  想要理解JavaScript中闭包的概念。首先我们先要了解JavaScript中的作用域链。

  当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

  上面的一段是JavaScript高级程序设计(第3版)对作用域链的描述。简单来说就是在函数中所能访问的变量按照层级关系所组成的一条有着先后顺序的链子。所以每个函数最先能访问的变量(也就是在这条链子上最先能接触到的)是当前函数的活动对象(就是在函数当中定义或重新赋值的变量),其次下一个能访问的变量就是当前的函数所在的包含环境。(其包含环境一般是指外部函数或全局执行环境。但在ES6当中块级代码语句也有可能生成包含环境。)然后是下下个包含环境。这样一层层的找下去,直到找到全局执行环境为止。

  所以这样我们就知道,能够访问一个函数A内部变量的除了这个函数A本身之外,其函数A所生成的包含环境中所在的函数B也可以访问。而知道了这一点我们就可以很容易猜到闭包的原理。那就是既然函数A内部所在的其它函数B可以访问到当前函数A的内部变量,那么如果我们将其内部所在的其它函数B作为返回值将其返回,并在函数A的外部用一个变量C来接收到这个返回值。那么这样,在函数A外部操作这个变量C时,实际上就是在操作函数A的返回值,也就是函数A所生成的包含环境中所在的内部函数B。而这个内部函数B是有权访问到函数A的内部变量的,所以在函数A外部的变量也就可以访问到函数A内部的变量。

  这样就实现了在函数外部访问到函数内部变量的操作。

  同时因为闭包的这一作用,也就有了闭包的另一个作用:被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。也就是说被函数A外部的变量C所访问到的函数A的内部变量,将会和变量C一同存在。即使函数A已经运行完毕,其内部变量也不会被JavaScript的自动回收机制回收掉。

  因为此时,函数A的内部函数B依然在访问函数A的内部变量。而内部函数B被返回给了函数A外部的变量C。此时变量C就代表了内部函数B,而又因为变量C位于函数A的外部。所以函数A的结束运行,并不能影响到变量C的值,所以即使函数A结束运行。但变量C的值依然存在,而变量C的值函数A的内部函数B。而函数B是有权访问函数A的内部变量的,所以就算函数A结束运行,按照逻辑变量C依然要可以访问到函数A的内部变量。而这就与我们说知道的变量生命周期产生了冲突。

  然而只要了解了JavaScript的自动回收机制,就会发现这一切并不意外。

JavaScript 中最常用的垃圾收集方式是标记清除(mark-and-sweep)。当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。可以使用任何方式来标记变量。比如,可以通过翻转某个特殊的位来记录一个变量何时进入环境,或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。说到底,如何标记变量其实并不重要,关键在于采取什么策略。垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

  上面一段是JavaScript高级程序设计(第3版)中对于avaScript的自动回收机制的描述。其中有这么一段从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。所以当函数A的内部变量被内部函数B访问时,此时函数A的内部变量就进入了函数B的环境中。而前面就说到,函数A的结束执行并不会影响到函数A外部的变量C,也就是内部函数B。所以内部函数B的环境会一直存在,直到变量C被注销。因而函数A的内部变量也会一直存在到变量C的注销为止。

以上就是我个人对于闭包的理解。希望对大家有所帮助!

最后要注意:

  • 闭包只能取得包含函数中任何变量的最后一个值。因为别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量。
  • 在确定绝对需要的地方才使用闭包。因为闭包到制变量不会被自动回收这一特性,可能会导致内存泄漏。

我的个人网址 http://www.wangyiming19950222.com/

阅读更多
换一批

没有更多推荐了,返回首页