函数的递归和堆栈
之前学递归一直学的迷迷糊糊,感觉懂了又感觉没懂,今天正好学习到了这一部分。
当函数解决一个任务时,在解决的过程中它可能会调用很多其他函数。当函数调用自身时,就是所谓的递归。
举一个例子:
一个函数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)
,递归变体经过了下面几个步骤:
pow(2, 4) = 2 * pow(2, 3)
pow(2, 3) = 2 * pow(2, 2)
pow(2, 2) = 2 * pow(2, 1)
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
,所有函数的处理都是一样的:
- 当前上下文被“记录”在堆栈的顶部。
- 为子调用创建新的上下文。
- 当子调用结束后 —— 前一个上下文被从堆栈中弹出,并继续执行。
下面是进入子调用 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