Fork me on GitHub
The hard ways

闭包的作用

这里我们将主要关注闭包的作用。至于到底什么是闭包,可以参考 闭包

如果被问到「闭包有什么作用?」想必有同学首先会想到「模拟私有变量」,又或者「在回调中有时会用到」,又或者最接近正确答案的「用于捕获变量」等等诸如此类。

这些回答显然都是正确的,不过它们并不是「闭包的作用」,而是闭包的一些具体的使用场景。其实如果被问到「闭包的作用」,那么这个问题本身就模棱两可,问一个东西的作用时,需要限定它的适用场景,把它放到一个前置环境下,再问它的作用。好比我问你平口起子的作用,你如果回答用它来在课桌上划三八线,我觉得也未尝不可。

而如果什么都不限定的问一句「平口起子的作用是什么」,那么我们从「平口起子最初是为什么被制造的」这个点来回来,才算是答其所问了。同样的,如果被问到「闭包有什么作用」,那么回答为什么需要闭包,才是答其所问,而不是闭包都能干什么。

当明白了问题之后,就已经成功了一大步。

作用域

首先我们知道编程语言中有一个「作用域」的概念,那么为什么是作用域呢?作用域就是程序中的「一段范围」在这个范围之内,某些变量是有效的。那么为什么要提出这个概念呢?没有作用域就不能编程吗?当然不是,没有作用域也是可以编程的,我们知道机器语言就没有作用域的概念,我曾经见过有前辈直接使用机器码编写出音乐播放器。

那么既然没有作用域也能编程,为什么要引入这个概念呢?这是因为高级编程语言中要尽量的提供丰富地表达程序的能力,因此提出了变量名的概念,而编程中的词汇是相对匮乏的,毕竟编程是一定程度上的对现实抽象的内容。想象一下,如果程序中对于同一个变量名只能使用一次,那么必定是一个噩梦,比如我们在写循环的时候经常使用 i,这下好了,程序中只能出现一个 i,其余都得是 i1 i2 ... iN 这样了。

有了作用域之后,我们在每个作用域中都能使用 i 了,这样就使得大家的词汇量得到了解放,也一定程序上使得程序表达更加简洁清晰,否则因为是程序中第 100 个循环就使用变量 i100 多少会让人感到有点傻。

一段范围

注意我们在介绍作用域中提到的:作用域就是程序中的「一段范围」在这个范围之内,某些变量是有效的。这个一段范围,在现有的编程语言实现中具有两种不同的解释:

  1. 静态作用域,又称为词法作用域
  2. 动态作用域

静态作用域

在具有静态作用域的语言中,它们对「一段范围」的解释是,代码中的一段范围。换句话说,变量的作用域是直接体现在代码中的、即静态的;在解析阶段就可以确定的、即词法的。静态作用域的好处就是,通过阅读代码,我们和解析程序就能够确定一个变量的作用域,当然就很方便理解了。

动态作用域

在那些使用动态作用域的语言中,它们对「一段范围」的解释是,程序执行中的某个时间点。换句话说,变量的作用域是又程序运行阶段的行为确定的,是不可预测的,因此在人肉确定变量的作用域时会花费一些精力。

静态作用域的例子:

// 一段用于演示静态作用域的 C 语言程序
#include<stdio.h> 
int x = 10; 
  
int f() 
{ 
   return x; 
} 

int g() 
{ 
   int x = 20; 
   return f(); 
} 
  
int main() 
{ 
  printf("%d", g()); 
  printf("\n"); 
  return 0; 
} 

结果打印 10

我们在 g 函数中调用了 f,我们知道 f 中的 x 就是全局作用域下的 x,因此即使在 g 中我们定义了局部变量 x,仍然不会影响到 f 中的 x 的绑定关系。

动态作用域的例子:

int x = 10; 
  
int f() 
{ 
   return x; 
} 
  
int g() 
{ 
   int x = 20; 
   return f(); 
} 
  
main() 
{ 
  printf(g()); 
}

结果打印 20,注意这个程序并不是 C 程序了只是用来显示动态作用域的程序。

在动态作用域的语言中,如何确定变量的绑定关系取决于当前调用的作用域。当我们在调用 g 的之后,我们在其作用域下定义了 x,于是在 f 中,其内部的 x 将引用到当前调用的作用域下的 x,也就是 g 中当前的 x

闭包的概念的缘起

既然 C 语言也是词法作用域的,那么为什么 C 语言中没有闭包的概念呢?这是因为在严格意义上来说,在 C 语言中函数并不是一等公民,也就是说你不能够像创建其他数据类型的实例一样、在运行阶段动态的创建一个函数、并将这个函数在程序中来回传递。当然利用汇编或者非 POSIX 中的 JUMP 类指令或者 API,能够模拟出动态创建函数的功能,但是在语言层面上,是没有直接的支持的。

回到 JS 中,函数是一等公民,我们可以像创建普通类型的变量一样创建一个函数类型的变量,比如:

var f = function () {
  return x
}

在 C 语言中,我们从语言直接提供的语义层面压根做不到上面的功能,而在 JS 中做到了,随之而生的就需要如何对 x 给出合理的解释。

由谁来向谁解释呢?由对语言负责的组织、即标准委员会来像使用语言的人进行解释。为了延续程序其他部分的词法作用域的特质,动态创建的函数部分也维持这个语义,才能保证语言的连贯性和整体性(当然也有同时提供两种作用域的语言,比如 Perl)。为了达到这个目的,就引入了闭包的概念,之所以是引入,而不是创造,是因为闭包也是前人的研究成果,当然前辈研究出闭包的目的也是为了支持在词法作用域下把函数当成一等公民来使用。

小结

说了这么多,对于问题「闭包的作用」,其答案就是「支持在词法作用域下将函数当做一等公民来使用」,没错,就是它的发明者当初发明它的目的。和「起子的作用就是拧螺丝」是一个道理。当然本文还是从理解问题开始,到作用域、到最终引出闭包的作用这样一个循序渐进的过程来进行讲解。相信会比直接给出答案「支持在词法作用域下将函数当做一等公民来使用」这样有利于大家进行理解吧。

Made with gadget