关于闭包的更多思考

闭包使我们不必再刻意定义和执行阶段,以减轻思维负担。

同学,请你讲一下闭包

我相信很多 javascript 工程师(泛指任何部分或者主要使用 javascript 完成日常开发任务的软件工程师)都会在面试的时候被问到 「闭包」,我也不例外。幸运的是,我很早就将准确的定义背的滚瓜烂熟,这并不能帮助我写出更好的代码,但能让我通过那些艰难的面试。在软件行业,死记硬背知识点并不是个好习惯,但有时候却依然有用。比如应付非专业面试官的提问。

先不要忙着嘲笑这些面试官的肤浅,确实,他们根本不知道闭包到底是什么,能做什么,只会对着标准答案判断应试者的水平。但有一点他们没有做错,或者说他们的团队没有做错,对于 javascript 开发,闭包的确是神兵利器,掌握的越早越好,越深刻越好。以我看来,每一个学习 javascript 的人都应该花费一定的时间学习它,直到融会贯通。

闭包是什么?

闭包的标准定义是:函数始终能够访问包含它的代码块所定义的变量,即便此代码块已经执行结束。简单的例子:

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

var counter = createCounter();
counter();

注:函数 counter 能够访问包含它的代码块 createCounter 定义的变量 count,即便后者已经执行结束。

上面的例子中,counter 函数始终能够访问并更新 count 变量,看起来,counter 似乎和 count 之间产生了一段看不见摸不着但却真是存在的关系。很多书里面对这种关系有很复杂很抽象的定义,本质意思是说,创建函数对象时也会顺便复制一份函数所关联的变量。在我看来,这体现了 javascript 聪明的一面:忠实的还原定义对象使用对象两个不同阶段的环境。createCounter 是定义,counter 是执行,但是由于闭包的支持,定义 counter 时所拥有的物理条件(可访问可更新的 count 变量,初始值为 0),在执行时依然成立。

只是数据安全吗?

很多文章在谈论闭包时会反复提及数据私密(data privacy),这当然是闭包所提供给工程师们的好处。比如上面的例子中,count 存储计数器当前值,它可以且只可以被 counter 按照事先定义的方式修改,其他任何代码都不可能直接访问,更新,删除它。这无疑是非常强大的编程能力,能够实现难以计数的复杂商业逻辑。

很多编程语言如 Java 也能通过另外的方式达到相同的效果,但更复杂。你可能先得准备一个 class,定义 private 成员变量,定义 public 方法以更新此变量。然后初始化 class 的对象。你得学习前面所说的这些知识以便保证数据确实被妥善的保护起来。而 javascript 的语法元素更少,你只需要在函数中定义 count,然后返回另一个函数(counter)来更新它。结束。

数据安全是非常重要而且常见的,你一定会在各种应用程序中编写类似的逻辑以保证你的数据不会被「不怀好意」的其他代码所破坏,有些甚至是自己编写的代码。从这个角度来说,javascript 在这样的场合一定会极大的提高编程效率,因为可以用相比 Java 更少的代码量,语法元素(访问限制修饰符,类,成员变量和方法)来实现完全一样的功能。

以上的结论明显和现实有所出入,现实世界中,Java 依然在某些行业拥有绝对的地位,主要的原因并非因为技术,很多公司倾向于使用现有的技术方案完成工作,而不是事实上更好的新技术。因为这些公司的决策者所要考虑的并不是技术的领先性和绝对的高效率,而是其他可能更加复杂的东西。

回到语言本身,在数据安全之外,闭包所要表达的终极目标是什么?我在前一节已经有所提及:保证函数被定义和被执行时使用使用相同的环境。具体的说,就是外部变量。定义和执行当然是两个不同的阶段,在实际的项目中,这样的两个阶段甚至会处于不同的源文件,他们的物理环境当然是不同的。闭包却能在一定程度上保证逻辑环境是不变的。当我们在阅读这样的代码时,就不必总要切换上下文,以保证自己的想法是正确无误的,而闭包帮我们做到了了这一点:只要在定义时逻辑自洽,执行时自然也天衣无缝。

所以闭包的根本意义在于,使我们不必再刻意定义和执行阶段,以减轻思维负担,这是很有意义的。人类拥有无以伦比的创造力和思维能力,但只局限于专注某个具体问题时。如果没有闭包,我们就必须在每一处代码执行的地方思考这里的 count 到底存不存在,如果存在,指向的到底是谁。这无疑非常复杂,而且对于解决软件问题毫无帮助。

所以呢

前面我反复提到了环境,环境是定义阶段(也即是外部函数的执行阶段)创建的,也即是说,每一次的重新定义,都会新创建一份新环境,多个环境之间互不干扰。从理论上来说,它们甚至无法感知到彼此的存在。所以:

var counter1 = createCounter();
var counter2 = createCounter();

counter1 和 counter2 所访问和更新的 count 是两份 count,互不影响。

我们甚至可以通过传入参数影响新创建环境,比如,准备一个初始值而不总是使用 count = 0。

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

虽然使用 javascript 我们不能在运行的时候改变已定义函数的逻辑,但是可以通过任何可能的方式改变闭包的运行环境,间接影响函数逻辑。这并不是很容易就能分析得到最优解法。你必须准确的找到数据和逻辑的结合点,将其分离。将逻辑部分交给内部语句,然后期待外部输入一个预期范围的数据以形成特定的业务环境。这一定是比通过业务和功能的不同分割代码更抽象的编程实践,也更能训练思维能力。