到底什么是闭包

本文源于同名问题:

到底什么是闭包? - 前端开发


感觉楼里大部分回答太复杂了,过于理论化,文绉绉地绕来绕去,没抓住本质和精髓。抄书谁不会啊?其实闭包没那么复杂。

最简洁、直击要害的回答,我能想到的分别有这么三句(版权属于

@张恂老师

):

1、闭包是一个有状态(不消失的私有数据)的函数。

2、闭包是一个有记忆的函数。

3、闭包相当于一个只有一个方法的紧凑对象(a compact object)

上面这三句话是等价的,而其中第 3 句最精妙,可以指导何时、如何用好闭包,后面我会详细分析。

澄清概念

MDN(Mozilla Developer Network)上的闭包定义是这样的:

Closures are functions that refer to independent (free) variables (variables that are used locally, but defined in an enclosing scope). In other words, the function defined in the closure 'remembers' the environment in which it was created.

Closures - JavaScript

关于什么是闭包以及有哪些典型的用法(实例)、注意事项,MDN 其实已经解释得很清楚了,建议英文好的同学先耐心读完这篇好文。

我换个角度来谈谈。

首先,很明确——闭包是一个函数,一种比较特殊的函数。什么是函数?函数就是一个基本的程序运行逻辑单位(模块),通常有一组输入,有一个输出结果,内部还有一些进行运算的程序语句。所以,那些仅仅说闭包是作用域(scopes)或者其它什么的,是错误的,至少不准确。

MDN 还有一段解释:

A closure is a special kind of object that combines two things: a function, and the environment in which that function was created. The environment consists of any local variables that were in-scope at the time that the closure was created.


理解了以上这些概念,关于“什么是闭包”您的大脑中是否出现了下面这张图(用 UML 组成结构图来表示):

有实例有真相

让我们先回顾下传统函式的机理。

我们说普通函式自身是没有状态的(stateless),它们所使用的局部变量都是保存在函式调用栈(Stack)上,随着函式调用的结束、退出,这些临时保存在栈上的变量也就被清空了,所以普通函式是没有状态、没有记忆的。

例如下面的普通函式 inc(),不管执行多少次都只返回 1:

var inc = function () {
  var count = 0;
  return ++count;
};

inc(); // return: 1
inc(); // return: 1


为什么这样?这是因为这里的 count 只是一个普通函式的局部变量,每次执行函式时都会被重新初始化(被第一条语句清零),它不是下面例子中可以保持状态的闭包变量。

再来看闭包的例子。

这可以说是一个最简单的 JavaScript 闭包的例子,这里的 inc() 是一个闭包(函式),它有一个私有数据(也叫闭包变量) count(即函式中的第 2 个 count)。

var inc = (function () { // 该函数体中的语句将被立即执行(IIFE)
  var count = 0; // 局部变量 count 初始化
  return function () { // 父函式返回一个闭包(函式引用)
    return ++count; // 当父函式 return(即上一个 return)后,这里的 count 不再是父函式的局部变量,而是返回结果闭包中的一个闭包(环境)变量。
  };
}) ();

inc(); // return: 1
inc(); // return: 2

我还未研究过任何 JavaScript 引擎(解释器)的源码,所以只好根据常识与逻辑作些合理的推测。

在本例中第 2 个 count 作为闭包的私有数据,很可能是被 JS 引擎存放到了堆(Heap)上,而且是按引用(byref)来访问,所以可以保持状态,实现计数累加;而第 1 个 count 只是存放在函式调用栈(Stack)上的局部变量,于是那个 IIFE 父函式一退出它就被销毁了,它的作用主要是用来初始化(赋值)给担任闭包变量的第 2 个 count。

可见两个 count 虽然同名,却是两个截然不同的变量!

这点恐怕正是许多 JS 初学者(包括当年的我)屡屡见到闭包时,感到最为大惑不解的地方吧。我们以为父子函式里外两个同名的变量是一回事,而其实它们不是,也不知道这背后究竟发生了哪些变化。

关于上面提到的内存管理模型中栈与堆的区别,建议不熟悉的同学可以参考下图和文章:

MDN: Concurrency model and Event Loop


闭包 vs. 对象

实现同样的计数功能,不用闭包怎么写?同样以 JavaScript 为例,用传统的 OOP 来写:

var obj = {
  count: 0,
  inc: function () {
    return ++this.count;
  }
};
obj.inc(); // count: 1
obj.inc(); // count: 2

用闭包与用对象,区别在哪?

其实主要区别就一个:这里用的是普通对象 obj 的方法(函数)inc,让 count 作为 obj 的成员变量来保存数据。而前面第一个例子直接用闭包函数 inc 的话,连 obj 这个对象也可以省掉,让 count 直接成为 inc 闭包内部所保存的状态(环境)变量,这样写起来就比传统的 OOP 更为紧凑,前者用 inc(),而后者用 obj.inc(),尽管两者最终实现的功能和效果基本是一致的。

通过以上这两个小例子的比较,你可以充分体会到在 JavaScript 中,函数(functions)作为首席/头等公民(first-class object)的地位。由于有了闭包,加上在 JavaScript 中函数也是对象——一个函数可以像一个传统的对象那样拥有自己的属性、私有数据和状态(不会随着栈而清空),许多简单功能的实现可能无需再借助 objects 了。

关于闭包与对象之间的联系,MDN 也说得很清楚:

A closure lets you associate some data (the environment) with a function that operates on that data. This has obvious parallels to object oriented programming, where objects allow us to associate some data (the object's properties) with one or more methods.

Consequently, you can use a closure anywhere that you might normally use an object with only a single method.


你看,闭包是不是相当(近似)于一个只有一个方法的对象?只有一个公开方法,而且成员数据几乎全私有(闭包)的对象,自然是一个紧凑版的对象了。

实际应用

读完本文,估计您对“到底什么是闭包”已经有了比较准确、细致的了解。下一个关心的问题,一定是:闭包在 Web 和 JavaScript 开发中有哪些实际的应用呢?

最后,推荐您继续阅读我的这篇文章,看看闭包作为紧凑对象的一个经典应用,如何利用一个非常简单的闭包来实现前后端开发中普遍存在的二元状态切换(toggle)功能:

JavaScript 设计模式之开关闭包(Closure Toggle)

参考

MDN: Closures - JavaScript

Wikipedia: Closure (computer programming)

编辑于 2016-06-16 20:25