函数与闭包的前世今生(四)

函数与闭包的前世今生(四)
0

本系列的其他文章:
函数与闭包的前世今生(一)
函数与闭包的前世今生(二)
函数与闭包的前世今生(三)

2.3、子程序与函数

计算机科学家和工程师很快就发现,有一些程序功能经常被用到,在很多地方都需要用到,例如计算某个数的正整数次方、排序一个数组(array)、将存储器的某一大块数据从一个地方复制到另一个地方等等。如果每次使用这些功能的时候都将实现这些功能的代码重新写一次,那么就很浪费代码的存储空间。所以计算机科学家和工程师决定将一些会反复用到的功能的代码抽离出来,抽离出来的这些小功能代码就叫做子程序 (subroutine) 或者函数。要用到这些功能的时候,先跳转到子程序的代码里执行,执行完再跳转回原来的地方继续执行。

  • PS:注意,在这里我们定义子程序或者函数为一些代码,而不包括这些代码所处理的数据。因此我在《函数与闭包的前世今生(一)》中倾向于把闭包定义成函数与其能访问的自由变量所组成的词法环境而不是能访问自由变量的函数

但是把一些代码抽离出来作子程序会带来 4 个问题:

  1. 这些子程序可能需要一些输入参数,这些输入参数如何从子程序的调用方传递给子程序?
  2. 子程序怎么知道执行完之后应该返回到什么地方(函数的返回地址)继续执行?
  3. 子程序的执行结果(函数的返回值)怎么传递给调用方?
  4. 子程序里面可能用到一些临时的存储空间(局部变量),这些存储空间应该如何分配?

在此解释一下第 4 条。在没有子程序的情况下,这些功能代码需要用到临时的存储空间时,可以使用全局的存储空间,即程序开始运行时就分配好的空间。但是有子程序而且子程序可能递归调用自己的话,如果用全局存储空间,那么在递归调用自己的时候,第二次调用就可能覆盖掉第一次调用的值。

2.2.1、函数的底层实现

由于函数调用具有先调用的函数后返回、后调用的函数先返回的 First In Last Out 的特点,我们自然而然地可以想到用栈 (stack) 这个数据结构来解决上述 4 个问题中的 1、2、4:

  1. 函数的输入参数由调用方压入栈中,函数中需要用到输入参数时从栈上获取;
  2. 函数调用方在调用函数前将返回地址也压入栈(push)中,函数返回时将返回地址从栈上弹出并写到当前指令地址寄存器中;
  3. 函数中要用到的局部变量在栈上分配空间,函数返回前将局部变量占用的栈空间释放掉。

用一段 JavaScript 代码举个例子:
当前准备执行第 16 行的函数调用:
image

将输入参数和返回地址压入栈中,在栈中开辟局部变量的空间,开始执行函数中的代码:
image

start = 0 小于 end = 1,跳过第 3、4、5 行。第 6 行计算出 mid 为 0。target = 2 大于 arr[mid] = 1,跳过第 7 到第 10 行,进入第 11 行:
image

再次将输入参数和返回地址压入栈中,在栈中开辟局部变量的空间,开始执行函数中的代码:
image

start = 1 不大于 end = 1,跳过第 3、4、5 行。第 6 行计算出 mid 为 1。target = 2 等于 arr[mid] = 2,进入第 8 行:
image

第二次函数调用返回(清理局部变量占用的栈空间,从栈上获取并跳转到返回地址,清理输入参数占用的栈空间):
image

第一次函数调用返回(清理局部变量占用的栈空间,从栈上获取并跳转到返回地址,清理输入参数占用的栈空间):
image

我觉得还是先将现在的电子计算机的核心组成说清楚,再来说函数与闭包的底层实现,所以把这篇从“三”改成“四”,插入一篇“三”来说明现在的电子计算机中 CPU、内存和硬盘的功能和关系。