函数进阶之递归初识和实例分析

243 阅读5分钟

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

认识递归

什么是递归?

递归(英语:recursion)在计算机科学中是指一种通过重复将问题分解为同类的子问题而解决问题的方法。

当一个函数解决一个任务时,在解决的过程中它可以调用很多其它函数。在部分情况下,函数会调用 自身。这就是所谓的 递归

递归的强大之处在于它允许用户用有限的语句描述无限的对象。因此,在计算机科学中,递归可以被用来描述无限步的运算,尽管描述运算的程序是有限的。——尼克劳斯·维尔特

思考题

写一个函数pow(x, n),实现 xn 次方,该怎么做?

x的n次方,就是x 乘以自身 n 次。可以直接用for循环,如

function pow(x, n) {
  let result = 1;
  // 再循环中,用 x 乘以 result n 次
  for (let i = 0; i < n; i++) {
    result *= x;
  }
  return result;
}
pow(2, 3) // 2*2*2 = 8

还可以利用递归的思路,简化任务,调用自身

function pow(x, n) {
  if (n == 1) {
    return x;
  } else {
    return x * pow(x, n - 1);
  }
}

现在我们来分析一下递归的思路,当pow(x, n) 被调用时,执行分为两个分支

  • 当n==1时,如pow(2, 1) = 2*1 = 2,它会立即产生明显的结果,这叫做 基础 的递归
  • 反之,利用x * pow(x, n - 1)表示pow(x, n),在数学中可能会写为image-20211104234944225

这叫做一个递归步骤,如计算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

可以看到pow递归地调用自身直到 n == 1。

因此,递归就是将函数调用简化为一个更简单的函数调用,然后再将其简化为一个更简单的函数,以此类推,直到结果变得显而易见。

image-20211104234433626

递归深度

最大的嵌套调用次数(包括首次)被称为 递归深度,如上面的示例,它的递归深度正好等于n。最大递归深度受限于 JavaScript 引擎。

执行上下文和堆栈

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

一个函数调用仅具有一个与其相关联的执行上下文。当一个函数进行嵌套调用时,将发生以下这些事

1)、当前函数被暂停

2)、与它关联的执行上下文被一个叫做执行上下文堆栈 的特殊数据结构保存;

3)、执行嵌套调用;

4)、嵌套调用结束后,从堆栈中恢复之前的执行上下文,并从停止的位置恢复外部函数。

实例分析

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

image-20211105001741072

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

image-20211105002117828

函数开始执行时,条件 n == 1 结果为假,所以执行流程进入 if 的第二分支,此时变量相同x = 2, n = 3,但执行流程在函数的第 1行。此时的上下文描绘为

image-20211105002137417

为了计算 x * pow(x, n - 1),我们需要使用带有新参数的新的 pow 子调用 pow(2, 2)。进入子调用 pow(2, 2) 时的上下文堆栈。

image-20211105002659425

现在重复上面的过程,在第 5 行生成新的子调用pow(2, 1),现在的参数是 x=2, n=1。新的执行上下文被创建,此时,有 2 个旧的上下文和 1 个当前正在运行的 pow(2, 1) 的上下文。

image-20211105002930534

在执行 pow(2, 1) 时,与之前的不同,条件 n == 1 为真,此时不再有更多的嵌套调用,所以函数结束,返回 2。函数完成后,就不再需要其执行上下文了,因此它被从内存中移除。前一个上下文恢复到堆栈的顶部:

image-20211105004026492

恢复执行 pow(2, 2)。它拥有子调用 pow(2, 1) 的结果,因此也可以完成 x * pow(x, n - 1) 的执行,并返回 4。然后,前一个上下文被恢复:

image-20211105004051594

恢复执行 pow(2, 3)。它拥有子调用 pow(2, 2) 的结果,当它结束后,我们得到了结果 pow(2, 3) = 8

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

从以上我们可以看到为了执行嵌套调用,JavaScript 会在 执行上下文堆栈 中记住当前的执行上下文。函数的处理流程为

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

参考资料:

Recursion and stack

Execution Contexts

Recursion (computer science)


🎨【点赞】【关注】不迷路,更多前端干货等你解锁

往期推荐

👉  你真的了解JavaScript的解构赋值吗?

👉 超详细的JS映射(Map)总结

👉 图解JavaScript的垃圾回收机制

👉JavaScript里this的绑定规则