6

闭包

在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。 --- 维基百科

其实这段引用已经说明了闭包的本质:引用了自由变量的函数自由变量将和这个函数一同存在——这是理解闭包的关键。

一 原理解释

函数式编程语言的基础是lambda演算,而闭包又是从函数式编程衍生而来。下面先从Lambda演算理解下函数式思维。(不感兴趣可直接跳过

Lambda演算

Lambda演算是一套用于研究函数定义、应用和递归的形式系统。它包括一条变换规则(变量替换)和一条函数定义方式,Lambda演算之通用在于,任何一个可计算函数都能用这种形式来表达和求值。因而,它是等价于图灵机的。尽管如此,Lambda演算强调的是变换规则的运用,而非实现它们的具体机器。可以认为这是一种更接近软件而非硬件的方式。Lambda演算对函数式编程语言有巨大的影响,比如Lisp和Haskell。

非形式化的描述

在lambda演算中,每个表达式都代表一个函数,这个函数有一个参数,并且返回一个值。不论是参数和返回值,也都是一个单参的函数。可以这么说,lambda演算中,只有一种“类型”,那就是这种单参函数。

:在函数式编程语言中,函数可是一等公民。

函数是通过λ表达式匿名地定义的,这个表达式说明了此函数将对其参数进行什么操作。例如,“加2”函数f(x)= x + 2可以用lambda演算表示为λx.x + 2 (或者λy.y + 2,参数的取名无关紧要)

λ演算中函数只有一个参数,那有两个参数的函数怎么表达呢?可以通过lambda演算这么表达:一个单一参数的函数的返回值又是一个单一参数的函数闭包吗?)。
例如,函数f(x, y) = x + y可以写作:

λx.λy.x + y  ----->λx. (λ y. + x y)

上面这个转化就叫currying,它展示了,我们如何实现加法(假设+这个符号已经具有相加的功能)。

其实就是我们现在意义上的闭包——你调用一个函数,这个函数返回另一个函数,返回的函数中存储保留了调用函数的变量。currying是闭包的鼻祖。(如果理解困难,下面会用编程语言实现上面的演算)

闭包解释

闭包被广泛使用于函数式编程语言,慢慢很多命令式语言也开始支持闭包。在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。

典型的支持闭包的语言中,通常将函数当作第一类对象——在这些语言中,函数通常有下列特性:
- 可以将函数赋值给一个变量
- 函数可以作为参数传递
- 函数的返回值可以是一个函数

例如以下Scheme(Lisp的一个方言)代码:

(define (f x) (lambda (y) (+ x y)))

在这个例子中,lambda表达式(lambda (y) (+ x y))出现在函数f中。当这个lambda表达式被执行时,Scheme创造了一个包含此表达式以及对x变量的引用的闭包,其中x变量在lambda表达式中是自由变量。

下面是用ECMAScript (JavaScript)写的同一个例子:

function f(x){ 
    return function(y) { 
        return x + y; 
        }; 
    }

其中f返回的匿名函数与其自由变量x组成了一个闭包。

上述代码在nodeJS环境中执行:

console.log( (f(7)) (2) );
//9

首先用第一个参数(7)代替最外层函数的参数(x),然后用第二个参数(2)代替第二层函数的参数(y),最终得到计算结果。

注意:这个运算执行了两个函数:f匿名函数。f的作用域为(f 7),这就是说,当(f 7)执行后,f这个函数就结束了,而x是f的私有变量,理论上x应该被释放了,然后x在f函数执行结束后并没有被释放,而是继续被匿名函数继续使用。支持这种机制的语言称为支持闭包机制在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部函数的变量,即使已经离开了外部函数的环境,自由变量(外部函数的变量)也和内部函数一同存在,则产生闭包)。

二 闭包的实现

通过上面的原理解释我们提出了这样一个问题(虽然这样的问题不干扰你理解闭包):

如果一个函数定义在栈中,那么当函数返回时,定义在函数中的局部变量就不复存在了, 那为什么内部的函数可以访问外部函数的变量?即使外部函数执行完,外部函数的变量也能和内部函数一同存在?

下面以JavaScript闭包实现举例。

javascript<script type="text/javascript" language="javascript">
    function a(){
        var i=0;
        return function b() {
            alert(++i);
        }
    }
    c=a();
    c();
</script>

上面是一个使用闭包的简单示例,代码执行完毕后,函数对象并不会被垃圾回收机制回收1,函数内的临时变量能够得以长期存在,而这个变量只能够被闭包函数修改,在外部是无法访问和修改的。(这个其实前面已经说过,这里有点啰嗦)

JavaScript中将作用域链描述为一个对象列表不是绑定的栈。每次调用JavaScript函数的时候(函数也是对象),都会为之创建一个新的对象用来保存局部变量,把这个对象添加至作用域链中。

每个函数关联都有一个执行上下文场景(Execution Context) ,然后执行环境会创建一个活动对象(call object),该对象包含了两个重要组件,环境记录,和外部引用(指针)。环境记录包含了函数内部声明的局部变量和参数变量,外部引用指向了外部函数对象的上下文执行场景。这样的数据结构就构成了一个单向的链表,每个引用都指向外层的上下文场景。最后形成如下图的结构:
图片描述
注:此图盗用,此图网站已不能打开。

如上图和代码所示:a返回函数b的引用给c,函数b的作用域链包含了对函数a的活动对象的引用,也就是说b可以访问到a中定义的所有变量和函数。函数b被c引用,函数b又依赖函数a,因此函数a在返回后不会被GC回收。

三 闭包的作用

以上述代码为例:
- 保护函数内的变量安全:函数a中i只有函数b才能访问,而无法通过其他途径访问到,因此保护了i的安全;
- 在内存中维持一个变量:函数a中i的一直存在于内存中,因此每次执行c(),都会给i自加1;
- 通过保护变量的安全实现JS私有属性和私有方法(不能被外部访问)。

Singleton 单件:(盗用理解Javascript的闭包的例子)

var singleton = function () {
    var privateVariable;
    function privateFunction(x) {
        ...privateVariable...
    }

    return {
        firstMethod: function (a, b) {
            ...privateVariable...
        },
        secondMethod: function (c) {
            ...privateFunction()...
        }
    };
}();

这个单件通过闭包来实现。通过闭包完成了私有的成员和方法的封装。匿名主函数返回一个对象。对象包含了两个方法,方法1可以方法私有变量,方法2访问内部私有函数。需要注意的地方是匿名主函数结束的地方的'()’,如果没有这个'()’就不能产生单件。因为匿名函数只能返回了唯一的对象,而且不能被其他地方调用。这个就是利用闭包产生单件的方法。

四 概念混淆之匿名函数

匿名函数指的是没有函数名称的函数,它和闭包没有关系,只是闭包中函数可以通过匿名函数编写,当然匿名函数不止会出现在闭包中。
如下面一个javascript代码示例:

var f = function(name){ 
    //函数体 
};

函数表达式是创建了一个匿名函数,然后将匿名函数赋值给一个变量f。

参考

理解Javascript的闭包
闭包漫谈(从抽象代数及函数式编程角度)
维基百科-闭包
维基百科-λ演算
编程语言的基石——Lambda calculus

脚注


  1. 注:Javascript的垃圾回收机制:
    在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数a被b引用,b又被a外的c引用,这就是为什么函数a执行后不会被回收的原因。 


人云思云
4.5k 声望525 粉丝

爱技术&爱艺术&爱生活