从知道 Javascript 这门语言以来,一直不断听到身边的人提到 Javascript 的闭包,也有很多人跟我解释过 Javascript 的闭包是怎么回事,但是,作为一个主力语言是 Java 的程序员,天资愚钝,不管别人跟我怎么解释,我怎么都无法很好地理解 Javascript 的闭包。

前段时间由于项目需要,写了不少的 Javascript 代码,不过都是照前端同学的代码依样画葫芦,里面也包含很多对闭包的使用,虽然代码可以跑,没有错,但是不理解,心里憋得难受,遂花了一点时间去“深入”理解了一下 Javascript 的闭包。

那么我们先来看看维基百科中闭包的定义:

在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。

一眼看这个定义,相当抽象,但是抛开抽象的名词,仔细分析一下我们可以发现只要理解 变量离开了创造它的环境还可以和引用它的函数一同存在 是怎么回事儿就可以。那么,要理解这句话,首先要理解 ECMAScript 标准中的几个概念:执行上下文(Execution Context),变量环境(VariableEnvironment)和 Function 对象的 [[scope]] 属性,下面先来看看这几个概念:

执行上下文

执行上下文是 ECMAScript 标准中定义的一个需要标准的实现者实现的行为。在一段 Javascript 代码中,最外层的 Global Object 关联一个全局的上下文,每执行一个方法就会进入到一个新的执行上下文中,方法执行结束就从这个执行上下文中退出,回到原来的执行上下文中,如果新执行一个方法,那么就又会进入到一个新的执行上下文中,这样,一个个的执行上下文就形成了一个栈,栈顶就是当前正在执行的方法的执行上下文。

PS,如果你是一个 Java 程序员,那么执行上下文并不难理解,它很像 Java 虚拟机中的 Stack Frame,我们知道 Java 每执行一个方法,都会在 VM Stack 中压入一个 Stack Frame,执行完后就弹出这个 Stack Frame,栈顶就是正在执行的方法,这和执行上下文基本上是一样的。

变量环境

Javascript 中每执行一个方法,都会创建一个执行上下文,当执行上下文创建完成以后,Javascript 会在执行上下文中定义一个变量环境,所谓的变量环境,就是用来存放在该执行上下文中创建的各种变量(包括方法变量),就像下面这段代码这样:

变量环境

[[scope]] 属性

当然,变量环境没有这么简单,相互之间并不是没有关系,每一个 Javascript 的 Function 对象都一个 [[scope]] 内部属性,这个属性指向的是 Function 对象所在的执行上下文的变量环境。对于上面例子中的 outer 方法,它的 [[scope]] 属性指向的就是全局执行上下文的变量环境,而 inner 方法的 [[scope]] 属性指向的是 outer 方法的执行上下文的变量环境。

每进入一个方法的代码,Javascript 都会把方法对象的 [[scope]] 属性拿来初始化变量环境,在声明绑定实例化的时候,再将在方法代码中定义的变量加入到变量环境中,因此,每一个变量环境都继承了外一层的变量环境,对于上面这段代码来说:

最外层的全局的变量环境就是:

{
    global: "global"
}

里面的 outer 方法的变量环境就是:

{
    global: "global",
    outer: "outer"
}

再往里面的 innter 方法的变量环境就是:

{
    global: "global",
    outer: "outer",
    inner: "inner"
}

闭包

从上面的我们得知,[[scope]] 起到了桥接内部和外部的变量环境的作用,那么,还是看上面的那段代码,inner 方法被返回,在其变量环境中的所有的变量,当然也都会随着 inner 方法被返回而继续存在,这就意味着在 outer 方法中定义的变量 outer,即使在 outer 方法执行结束以后还会继续随着 inner 方法存在,这个时候 outer 就成为了自由变量,它离开了创造它的环境(outer 方法)还和引用它的函数(inner)一起存在,这就是前面我们提到的闭包的定义。

结论

从上面的分析思路来看,闭包差不多就是变量环境的继承而导致的副作用物。了解了闭包产生的过程,以后使用的时候就不会再感到无所适从了。当然,这个“副作用物”的威力有多大,现在初学 Javascript 的我还没有领会到,如果大家有这方面的经验,不吝赐教。

参考资料