闭包(词法闭包)的定义

闭包是「函数」以及该函数相关的「引用环境」组合而成的实体。

这个定义中,包含了两个核心的要素:

  1. 函数:一段可以执行的代码
  2. 引用环境:生成闭包时的词法环境

观察以下这段 JavaScript 代码:

1
2
3
4
5
6
7
8
9
10
11
function outer() {
  var _private = 'private'

  return function inner() {
    return _private
  }
}

var getPrivate = outer()
console.log(getPrivate.name) // 'inner'
getPrivate() // 'private'

函数 outer 调用后,会将在其内部定义的函数 inner 暴露出来并赋值给 getPrivate,在下一行代码可以确认这点。
再继续下一行,我们调用 getPrivate,发现成功得到了 outer 的本地变量 _private,尽管此时 outer 已经返回(我们学习过的编程知识告诉我们,函数执行时在栈上分配的变量会在离开函数执行环境时被销毁)。这个例子的实际结果说明 outer 的本地变量 _private 被保存在某个地方(堆上分配)继续可用。

根据这些表现,我们可以确认这就是一个典型的闭包。

实际上,这个闭包在 outer 被调用的时候创建。它在某个地方保存了函数 inner 所需要的引用环境,使得离开了创建闭包的环境(outer)时,对自由变量的引用依旧有效(直至闭包的生命周期结束才一并被回收)。

注:自由变量是指在函数之外的定义的变量,它既不是本地变量,也不是参数。


闭包的实例

按照上面的定义,闭包不能简单等同于函数(除非语言对函数的实现符合上面的定义),因为它还包含了跟函数组合的词法环境。 同一个函数,跟不同的环境组合,能得到不同的闭包实例。

继续看代码:

1
2
3
4
5
6
7
8
9
10
function inc(x) {
  return function() {
    x += 1
    return x
  }
}
var incA = inc(0) // 闭包实例1
var incB = inc(0) // 闭包实例2
incA() // 1
incB() // 1

在这段代码中,函数 inc 每进入一次,就形成一个新的环境,内部函数跟这个环境结合,生成了不同的闭包的实例。
两次调用 inc 返回了两个不同的闭包实例,这两个实例的函数相同,但环境不同。两个环境拥有各自的 x 的绑定,互不干扰。


闭包的实现原理

理解了闭包的定义和特点后,就能理解闭包的实现方式了。 闭包可以表示为一个数据结构,这个数据结构中,保存了函数地址指针,也保存了闭包创建时的函数所处的词法环境。典型的闭包实现方式就是这么做的。

JavaScript 的函数,正好实现了上述的数据结构。

在 JavaScript 中,函数本身也是对象。所以,虽然罕见,但是函数确实是可以用下面这种创建对象的方式来创建的:

1
var f = new Function('x', 'return x')

这相当于:

1
2
3
var f = function(x) {
  return x
}

既然函数是一个对象,那么就可以往这个对象身上保存信息。
函数对象拥有一系列内部属性或内部方法,在 ES 规范中,一般用双重方括号来表示内部属性和内部方法。 在这里我们需要关注的属性不多:

  1. [[Scope]] 内部属性:函数在创建的时候,会把当前「执行环境」的「词法环境」赋给这个内部属性(函数声明和函数表达式细节上略有差异,但不必深究)。换句话说,函数对象会记忆它被创建时所处的词法环境,用以查找函数引用的自由变量的绑定。这行为就像创建了一个封闭的上下文,将所用到的变量都绑定起来纳入其中(闭包名字的由来…)。
  2. [[Code]] 内部属性:保存函数的 ECMAScript 代码。
  3. [[Call]] 内部方法:这是函数对象区别于其它对象的一个接口,实现这个接口的对象称为可调用对象。

从这种实现方式可以发现,JavaScript 的函数本身就是一种闭包。


狭义的 JavaScript 闭包

在 JavaScript 中,理解函数实质是一种闭包对深入学习这门语言有帮助。不过在习惯上,我们说的闭包,一般是特指一些特殊情况下的函数:引用了自由变量的函数

再看看上文的第一个例子:

1
2
3
4
5
6
7
function outer() {
  var _private = 'private'

  return function inner() {
    return _private // 这就是自由变量
  }
}

函数 inner 就是一个闭包,它引用了定义在外层函数的变量 _private,这个变量既非参数,亦非本地变量,而是一个自由变量。

总结

  1. 闭包是函数 + 引用环境的组合体
  2. 同一个函数可以跟不同的环境结合成不同的闭包实例
  3. JavaScript 函数本身就是一种闭包
  4. 我们通常讨论的 JavaScript 闭包是指引用了自由变量的函数