函数的递归和堆栈

之前学递归一直学的迷迷糊糊,感觉懂了又感觉没懂,今天正好学习到了这一部分。

当函数解决一个任务时,在解决的过程中它可能会调用很多其他函数。当函数调用自身时,就是所谓的递归

举一个例子:

一个函数pow(x,n),计算x的n次方。

递归思路:

1 function pow(x, n) {
2   if (n == 1) {
3     return x;
4   } else {
5     return x * pow(x, n - 1);
6   }
7 }
8 
9 alert( pow(2, 3) ); // 8

函数执行分为两个分支:

1、如果n == 1,函数会立即产生明显的结果,这叫做基础的递归

2、else,这个分支叫做一个递归步骤:将任务转化为更简单的行为(x的乘法)和更简单的同类任务(带有更小的n的pow运算)的调用。接下来的步骤将其进一步简化,直到n到达1。

比如,为了计算 pow(2, 4),递归变体经过了下面几个步骤:

  1. pow(2, 4) = 2 * pow(2, 3)
  2. pow(2, 3) = 2 * pow(2, 2)
  3. pow(2, 2) = 2 * pow(2, 1)
  4. pow(2, 1) = 2

 

到这里之前也是懂的,接下来研究一下递归调用的工作原理。

函数底层的工作原理:有关正在运行的函数的执行过程的相关信息被存贮在其 执行上下文中。

执行上下文是一个内部数据结构,它包含有关函数执行时的详细细节:当前控制流所在的位置,当前的变量,this 的值(此处我们不使用它),以及其它的一些内部细节。

一个函数调用仅具有一个与其相关联的执行上下文。

当一个函数进行嵌套调用时,将发生以下的事儿:

  • 当前函数被暂停;
  • 与它关联的执行上下文被一个叫做 执行上下文堆栈 的特殊数据结构保存;
  • 执行嵌套调用;
  • 嵌套调用结束后,从堆栈中恢复之前的执行上下文,并从停止的位置恢复外部函数。

让我们看看 pow(2, 3) 调用期间都发生了什么。

pow(2, 3)

在调用 pow(2, 3) 的开始,执行上下文(context)会存储变量:x = 2, n = 3,执行流程在函数的第 1 行。

我们将其描绘如下:

  • Context: { x: 2, n: 3, at line 1 } call: pow(2, 3)

这是函数开始执行的时候。条件 n == 1 结果为 false,所以执行流程进入 if 的第二分支。

 

变量相同,但是行改变了,因此现在的上下文是:

  • Context: { x: 2, n: 3, at line 5 } call:pow(2, 3)

为了计算 x * pow(x, n - 1),我们需要使用带有新参数的新的 pow 子调用 pow(2, 2)

pow(2,2)

为了执行嵌套调用,JavaScript 会在 执行上下文堆栈 中记住当前的执行上下文。

这里我们调用相同的函数 pow,所有函数的处理都是一样的:

  1. 当前上下文被“记录”在堆栈的顶部。
  2. 为子调用创建新的上下文。
  3. 当子调用结束后 —— 前一个上下文被从堆栈中弹出,并继续执行

下面是进入子调用 pow(2, 2) 时的上下文堆栈:

  • Context: { x: 2, n: 2, at line 1 } pow(2, 2)
  • Context: { x: 2, n: 3, at line 5 } pow(2, 3)

新的当前执行上下文位于顶部(粗体显示),之前记住的上下文位于下方。

当我们完成子调用后 —— 很容易恢复上一个上下文,因为它既保留了变量,也保留了当时所在代码的确切位置。

pow(2,1)

这是当pow(2,1)时,函数的执行上下文堆栈,现在的参数是x=2,n=1,

新的执行上下文被创建,前一个被压入堆栈顶部:

  • Context: { x: 2, n: 1, at line 1 } call:pow(2, 1)
  • Context: { x: 2, n: 2, at line 5 } call:pow(2, 2)
  • Context: { x: 2, n: 3, at line 5 } call:pow(2, 3)

此时,有2个旧的上下文和一个当前正在运行的pow(2,1)的上下文。

出口

在执行 pow(2, 1) 时,与之前的不同,条件 n == 1 为 true,因此 if 的第一个分支生效: 

此时不再有更多的嵌套调用,所以函数结束,返回 2

函数完成后,就不再需要其执行上下文了,因此它被从内存中移除。前一个上下文恢复到堆栈的顶部:

  • Context: { x: 2, n: 2, at line 5 }call: pow(2, 2)
  • Context: { x: 2, n: 3, at line 5 }call: pow(2, 3)

恢复执行 pow(2, 2)。它拥有子调用 pow(2, 1) 的结果,因此也可以完成 x * pow(x, n - 1) 的执行,并返回 4

然后,前一个上下文被恢复:

  • Context: { x: 2, n: 3, at line 5 }call: pow(2, 3)

当它结束后,我们得到了结果 pow(2, 3) = 8

 

本示例中的递归深度为:3

递归深度:最大的嵌套调用次数(包括首次)。

从上面我们可以看出,递归深度等于堆栈中上下文的最大数量。

使用递归时需要注意内存,上下文占用内存。

 

参考文献:https://zh.javascript.info/recursion

 

posted @ 2020-04-15 12:46  LangZ-  阅读(799)  评论(0编辑  收藏  举报