设计闭包(Closure)的初衷是为了解决什么问题?

设计闭包(Closure)的初衷是为了解决什么问题?OOP设计模式不是更好,逻辑更清晰吗。
关注者
450
被浏览
18324

13 个回答

Closure 是为了在实现 λ-calculus 时搞定作用域问题而搞出来的,详见Closure (computer programming) - Wikipedia, the free encyclopedia
λx.λy.x+y 可能实现为
function add(x) => {
    return (y)=>{ x + y}
}
1+2可以这样表示:
let add1=add(1)
add1(2)

let add2=add(2)
add2(1)

这里的 add1、add2 都是 closure ,明显这俩都是 add 的某种“副本”,实现上需要有一块存储区域来记add1: x=1、add2: x=2 这个状态,这个状态对外不可见,所以叫它闭包还蛮形象的。

在现实的编程语言,往往将这个概念扩张一下:
function foo(){
    var a=0, b=0;
    return ()=>{a+b}
}

这种也称之为 closure,因为 a 和 b 的可见性确实封闭在 foo 函数范围之内了。这时候就跟 OOP 能扯到一起了,它提供了一个作用域,可以实现隐藏状态显露行为,正是 OOP 的核心理念。所以在 JavaScript 这类没有提供对象成员可见性控制的语言里,经常拿这个来模拟 OOP 的可见性。
闭包是函数式编程及其核心思想“Lambda 计算法”(Lambda Calculus)的必备基本设定。

我们都知道:
  • 函数式编程有一个特点,就是所有操作都用可计算的函数(computable function,下简称“函数”)来体现。
  • 函数的两个特点,就是每个函数都有一个输入值,一个输出值。
  • 函数还有一个定律,就是给定一个确定的输入值,总能得到一个确定的输出值,即输入输出有严格的一一对应关系。
  • (当然还有无副作用之类,此处不论。)

那么,在这样的限制条件下,请问,“加法”的函数怎么写?

function plus(senior) {
    return senior + 1;
}

是,这倒是 +1s 了,但是怎么加任意数值呢?加法的定义是 a + b,两个参数。记住,函数只能有一个输入,一个输出。

聪明的人们想到了一个办法,如果函数 A 拿到参数 ① 之后能够返回另一个函数 B,而函数 B 拿到参数 ②,再把参数 ① 和 ② 糅合起来就可以了。

function plusAny(senior) {
   return function(second) {
        return senior + second;
   }
}

/* usage */

var senior = 2838240000;
var longLiveSeniorFunc = plusAny(senior);

longLiveSeniorFunc(1);     // +1s
longLiveSeniorFunc(3600);  // +1h
longLiveSeniorFunc(86400); // +1d

(注意输入输出有严格对应关系,所以不管呼叫 longLiveSeniorFunc(1) 多少次结果都是一样。所以,每次 +1 前应该重新定义 longLiveSeniorFunc,使用最新的 senior。此处省略。)

有了此法,不管多少参数,都可以用此法来转换成一个参数。

不过等一下,这需要我们做一些设定:

  • 首先,认可“函数”和“数字”、“字符”等等一样,是一种“值”(value)
  • 然后,认可“函数”可以和其他值一样,可以赋给一个变量,返回,或者当做参数来传递

这意味着函数享有其他“值”的便利,可称之为一个语言的“一等公民”(first-class citizen)。不过,这时候这个函数还只是一个匿名函数(anonymous function),还不能称之为闭包。

而这个 plusAny 函数,就是一个闭包。因为它返回了一个函数,且此函数“包”进了 plusAny 函数作用域里的一个本地变量 senior,并且可以在其存续期间持续使用这个变量。

那么,为什么 A 函数内部定义的函数能访问 A 函数作用域的变量?为什么要这样设定?很简单,因为不这样设定,函数式编程就连加法都没法定义啊……

所以,闭包是函数式的基本配备。

当然,函数入魔的朋友们也搞出了很多滥用的幺蛾子,那又是后话了……


更新
-----

关于函数为什么只能有一个参数的问题,试分析一下。

第一是抽象。既然所有“多参数函数”都可以抽象成一个高级的“单参数函数”(用上面的方法),那么意味着“多参数函数”本身是有信息冗余的。这个抽象的过程就像是拆掉分子分母的公约数一样,去掉了这个冗余。

第二是计算。我们想象一下任何一种“计算机器”,它要做一个最简单的加法,那么它需要:

  1. 读取 a,保存;
  2. 读取 b,计算 a + b;
  3. 返回结果。

那么可以看出,在第 1 步时如果这个机器暂停,那么实际上它保存了一个“状态”(state),而没有进行任何“计算”。

如果把这个保存了某个状态的机器视作是一个新的机器(在内存里写死了 a 值,等待 b 值做加法),那么新的机器才是真正在做“计算”。原来什么值都没有保存的机器有没有在做计算呢?我们不妨认为它也在做“计算”,这个计算的参数则是 a 值,计算结果就是前面说的新机器。

如此,既确保了没有信息冗余(严格说第 2 步还可以继续拆),状态被作为显性的参数所捕捉,并且每一个步骤都是一次“计算”,且计算的结果和参数一一对应。

换句话说,就是函数成了“无状态”,它返回什么,完全且仅取决于你给它喂什么参数。只要你喂的参数不变,到世界末日它也会返回给你同样的结果。这进一步意味着这个函数是完全独立的,可以独立存在(比如抽成库或者服务),可以很容易地替换(只需确保输入输出对应关系),可以很容易地测试(只需检查输入输出)。

那么,为什么要这么严格而极致地来定义这样一套体系?因为先行者们想发明一套通用的、可计算任意算式的计算机架构,比如与 Lambda Calculus 同时期的图灵机。

了解了这些,就能领略函数式编程之美,并且可以用更批判的眼光看待各种编程语言,还会对架构设计,测试编写等大小方面有各种好处。

难怪会有人说这个极度抽象和反直觉的体系“让人上瘾”。
为什么?