什么是JavaScript闭包终极全解之一——基础概念

时间:2021-05-02 18:01:14

什么是JavaScript闭包终极全解之一——基础概念

“闭包是JavaScript的一大谜团。最近的一项调查显示,有关JavaScript的闭包的博客文章占23%左右” [1]

引子

在阮一峰博士的博客中[2],已经对JavaScript的闭包概念解释得非常详细,但是博主还是觉得有必要,对闭包这一名词以JavaScript为例,从概念到应用做更为深入研究,方便读者更为透彻的理解。

 

首先借用阮老师对闭包(closure)的概念做出的定义(或描述):

 什么是JavaScript闭包终极全解之一——基础概念

阮老师的理解是:“闭包就是能够读取其他函数内部变量的函数”。并且认为:“可以把闭包简单理解成‘定义在一个函数内部的函数’”。暂且先不评论这种定义是否合适、是否正确?(读者请注意,这里我没有说阮老师说的不对)

我们先来看看另一个对于闭包概念的相关描述。

在《JavaScript高级程序设计(第3版)》中文版中[3],具体描述在第7章函数表达式第7.2节(页码为第178页):闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。

先抛开对于两个出处的具体描述,至少两者对于闭包的定义可以简化为: “闭包是一种函数”,而且闭包是一种特殊的函数。以上两个出处涉及到了一些概念(或名词):函数、内部变量、函数内部、函数作用域。本文会对这些概念做出适度的相关解释。

概念

程序设计中的Closure

计算机程序设计(Computer Programming)中对于闭包(Closure)的定义是我们对于JavaScript闭包(Closure)理解的本质基础。 

In programming languages, closures (also lexical closures or function closures) are a technique for implementing lexically scoped name binding in languages with first-class functions.Operationally, a closure is a record storing a function together with an environment:a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or storage location to which the name was bound when the closure was created. A closure—unlike a plain function—allows the function to access those captured variables through the closure's reference to them, even when the function is invoked outside their scope. [4]

 什么是JavaScript闭包终极全解之一——基础概念

释义是:

在程序设计语言中[4],闭包(即词法闭包lexical closures或函数闭包function closures)是用一等函数[5](first class)实现词法作用域(lexically scoped)名称绑定[6](name binding)的一种技术。操作上说,一个闭包是一个用来存储一个函数及其环境的一条记录[7](record),这个环境会在闭包创建的时候,在函数的每个*变量[8](free variable——在局部使用,但是定义在外部作用域)与其值或名称绑定的存储区之间建立一个映射。不同于一般普通函数(plain function),即使当函数在其作用域外被调用时,闭包仍然允许函数通过闭包的引用访问(存取)捕获的变量(captured variables)。

 

以上释义比较抽象,我们先用JavaScript语言举一个简单的例子并加以说明(我在此采用*中说给出的例子并加以改写),在段代码与以上概念之间建立一个映射,让大家有一个直观的概念。稍后,我们再来理解这些概念是什么、如何理解?

 

 1 function   startAt(CAPTURED) {
 2 
 3 function incrementBy(i) {
 4 
 5       return CAPTURED + i;
 6 
 7 }
 8 
 9 return incrementBy;
10 
11 }

以上的代码片段定义了一个高阶函数(higher-order function)startAt,这个函数接收一个参数CAPTURED(我将由闭包捕获的变量大写。这并不是JavaScript的标准实践,也不鼓励这样做,这里只是为了方便说明[4])和一个内部函数(nested function) incrementBy。尽管CAPTURE变量不是incrementBy的局部变量,由于incrementBy处于CAPTURED变量的词法作用域中,这个内部函数可以访问变量CAPTURED。函数startAt返回了一个包含函数incrementBy的闭包,这个函数把变量i与CAPTURED相加然后和CAPTURED在当前(this)调用startAt的一个引用返回,所以当incrementBy被调用时会知道当前的CAPTURED的值。

应用以上函数得到运行结果如下:

 1 var   startAt10 = startAt(10); // closure1
 2 
 3 var   startAt100 = startAt(100); // closure2
 4 
 5 startAt10(1);
 6 
 7 >>   11
 8 
 9 startAt100(1);
10 
11 >>   101

我们注意到,因为startAt返回一个函数,所以变量startAt10 和 startAt100 是属于函数类型。运行startAt10(1)会返回11,运行startAt100(1)会返回101。尽管startAt10和startAt100都关联到同一个函数incrementBy,相关的环境却不同,调用闭包会将CAPTURED与不同值的不同变量绑定,因此执行函数会得到不同的结果。

关于*变量(free variable)和名称绑定(name binding)

*变量(free variable)

在计算机程序设计中[8],术语*变量(free variable)是指函数中使用的变量,它们既不是函数的局部变量也不是参数。在当前语境下,术语非局部变量(non-local variable)是它的同义词。 

与*变量(free variable)对应的术语是约束变量(bound variable),约束变量在某个状态之前是*的,在某个状态之后它会被绑定到一个或一组具体的值。在程序设计语言中,变量可分为*变量与约束变量两种。简单来说,局部变量和参数都被认为是约束变量;而不是约束变量的则是*变量。[9] 

在作者翻阅的众多JavaScript书籍中,只有Michael Fogus的《JavaScript函数式编程》[1]中文版对*变量有较为详细的说明,具体描述在第3章变量的作用域和闭包第3.5节闭包(页码为第55页),而书中对*变量的相关说明也只是描述解释性非定义性: 

“*变量与闭包的关系是,*变量闭合于闭包的创建。闭包背后的基本原理是,如果一个函数包含内部函数,那么它们都可以看到其中声明的变量;这些变量被称为‘*’变量。然而,这些变量可以被内部函数捕获,从高阶函数中return实现‘越狱’,以供以后使用。唯一需要注意的是,捕获函数必须在外部函数内定义。函数内没有任何局部声明之前(既不是被传入,也不是局部声明)使用的变量就是被捕获的变量。” 

书中给出一个例子(这个例子与*中给出的例子相似): 

 1 function   makeAdder(CAPTURED) {
 2 
 3 return function(free) {
 4 
 5       return  free + CAPTURED
 6 
 7 };
 8 
 9 }
10 
11 var   add10 = makeAdder(10);
12 
13 add10(32);
14 
15 //=>   42
16 
17  
18 
19 var   add1024 = makeAdder(1024);
20 
21 add1024(11);
22 
23 //=>   1035
24 
25  
26 
27 add10(98);
28 
29 //=>   108

如何对应例子理解上面一段话的内容? 

“如果一个函数包含内部函数,那么它们都可以看到其中声明的变量;这些变量被称为‘*’变量。”

——以上makeAdder函数包含了一个匿名的内部函数用以将free 和 CAPTURED两个变量相加。这里“它们都可以看到”中的“它们”,在语境下应该指示内部函数,内部匿名函数可以看到CAPTURED这个变量,因此CAPTURED变量被称为“*”变量。 

这里free是不是*变量呢?

——不是。这也是阅读时容易给读者造成误解的地方。显然,*中的定义:“*变量(free variable)是指函数中使用的变量,它们既不是函数的局部变量也不是参数”。而此处,free是内部匿名函数的参数,显然不是*变量。所以《JavaScript函数式编程》[1]中给出的例子,还是应该理解为原作者给出的一个“免费”的意思,而非译注上面理解的“*”变量。比较好的写法是在此语境下将free命名成别的名字,以免混淆。这也是我在改写*中的例子时,采用了CAPTURED的写法而抛弃了free命名。 

在《JavaScript函数式编程》[1]中:“唯一需要注意的是,捕获函数必须在外部函数内定义。函数内没有任何局部声明之前(既不是被传入,也不是局部声明)使用的变量就是被捕获的变量。”

在*Closure[4]中:“不同于一般普通函数(plain function),即使当函数在其作用域外被调用时,闭包仍然允许函数通过闭包的引用访问(存取)捕获的变量(captured variables)。” 

因此,简单来理解“捕获的变量”和“*变量”只是同一个变量分别在动态作用域(运行时)和词法作用域(静态时)的两种不同名称。

名称绑定(name binding)

在计算机程序语言中[6],名称绑定就是将实体(entities- data and/code)与标识符(identifiers)相关联的过程。我们将一个标识符绑定到一个对象上叫作引用(reference)该对象。机器语言没有内置的标识符记法(notion),但是名称与对象(name-object)绑定做为一个服务和记法(notation)被程序语言实现并且被程序员所使用。绑定与作用域的概念紧密相关,因为作用域从词法上,在计算机编码中定义了何名称在何种可能执行的路径下于何处绑定何具体对象。

相关名称绑定概念的具体分支在此篇中不做展开,会在后续系列中详细说明。

结语

在Mozilla的相关开发文档中[10],我们可以看到对于Closure的定义为:“Closures are functions that refer to independent (free) variables. In other words, the function defined in the closure 'remembers' the environment in which it was created.”。即“闭包是使用独立(*)变量的函数,换句话说,闭包定义的函数记录了它被创建时的环境。

 

总而言之,关于JavaScript闭包我做出以下理解(限于计算机程序设计语境下):

简单理解(基本)

闭包是一个函数

细化(来自阮一峰博客)

闭包是定义在一个函数内部,并且能够读取其他函数内部变量的函数

明确(来自《JavaScript高级程序设计》第三版)

闭包是定义在一个外部函数内部,并且能够访问(存取)外部函数中*变量的函数

广义抽象(来自Mozilla与*)

闭包是一个抽象的环境记录,它包含狭义的闭包函数以及在创建该函数时,每个*变量及其值或名称绑定存储区直接的一个映射。

 

参考

[1] Michael Fogus著 欧阳继超 王妮译 JavaScript函数式编程[M]. 人民邮电出版社,2015: 44-60

[2] http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html

[3] Nicholas C.Zakas著 李松峰 曹力译 JavaScript高级程序设计(第3版)[M]. 人民邮电出版社,2012: 178-182

[4] https://en.wikipedia.org/wiki/Closure_(computer_programming)

[5] https://en.wikipedia.org/wiki/First-class_function

[6] https://en.wikipedia.org/wiki/Name_binding

[7] https://en.wikipedia.org/wiki/Record_(computer_science)

[8] https://en.wikipedia.org/wiki/Free_variables_and_bound_variables

[9] http://blog.jobbole.com/47296/

[10] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

[11] David Herman著 黄博文 喻杨译 Effective JavaScript编写高质量JavaScript代码的68个有效方法. 机械工业出版社,2014: 31-34