JavaScript中的递归原理

162 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

递归的工作原理类似于循环在JavaScript中的工作方式。循环允许您多次执行一组代码,只要条件为真。

在循环中,当条件变为false时,执行将停止。如果执行的条件永远保持正确,你将获得一个无限循环,该循环可能会崩溃你的应用程序。 递归也是如此——只要递归的条件保持正确,递归就会一直发生,直到条件阻止它,否则,你会得到无限递归

什么是递归?

递归是一个函数调用自身的概念,并一直调用自己,直到被告知停止。

让我们看一个例子:

function printHello() {
  console.log("hello")
}

printHello()

在这里,我们声明一个printHello函数,将“你好”记录到控制台。然后,我们在定义之后调用函数。

在递归的情况下,我们也可以像这样从printHello函数中调用printHello函数:

function printHello() {
  console.log("hello")

  printHello()
}

printHello()

// hello - first function call
// hello - second function call
// hello - third function call
// and it goes on infinitely

这是递归。因此,当JavaScript执行printHello(),“hello”被打印到控制台,然后再次调用printHello()。以下是递归的发生方式:

  • printHello()第一次执行,将“你好”打印到控制台,就在函数中,再次调用printHello()
  • printHello()第二次执行,console.log("hello")再次运行,再次调用printHello()
  • printHello()第三次执行,console.log("hello")再次运行,再次调用printHello()
  • 它一直持续到通话堆栈达到最大值,应用程序崩溃:

呼叫堆栈错误

最大调用堆栈大小超过错误

让我们理解这个错误的含义。

什么是Call Stack?

调用堆栈是JavaScript用于跟踪当前正在执行的函数的机制。

当调用函数时,它会添加到调用堆栈中。例如,上面的这个函数:

function printHello() {
  console.log("hello")
}

printHello()

当要执行此函数时,它将添加到调用堆栈中:

// printHello()
// ----
// call stack

执行后(当所有代码运行时,或遇到return语句时),函数将从堆栈中弹出:

// ----
// call stack

例如,如果printHello函数调用另一个这样的函数:

function printHi() {
  console.log("hi")
}

function printHello() {
  console.log("hello")

  printHi()
}

printHello()

在这种情况下,当调用printHello时,调用堆栈将如下所示:

// printHello()
// ----
// call stack

运行console.log("hello")行后,下一行是printHi(),此调用将添加到堆栈顶部:

// printHi()
// printHello()
// ----
// call stack

printHi()调用完成执行后,它会从堆栈中弹出:

// printHello()
// ----
// call stack

printHello()完成执行后,它也会从堆栈中弹出:

// ----
// call stack

那么递归如何与调用堆栈一起工作呢?

递归和Call Stack

返回我们上面的递归代码:

function printHello() {
  console.log("hello")

  printHello()
}

printHello()

这里发生的事情是,当printHello()执行时,它会被添加到调用堆栈中:

// printHello()
// ----
// call stack

执行console.log("hello"),然后再次执行printHello(),并添加到调用堆栈的顶部:

// printHello()
// printHello() -- "hello"
// ----
// call stack

现在,我们目前在调用堆栈上有两个函数:第一个printHello和第二个printHello,这是从第一个函数调用的。

在执行第二个printHello期间,执行console.log("hello"),并再次调用printHello。现在堆栈如下所示:

// printHello()
// printHello() -- "hello"
// printHello() -- "hello"
// ----
// call stack

事情是这样的,我们没有任何条件可以阻止递归,所以printHello继续调用自己并填充堆栈:

// ...
// printHello() -- "hello"
// printHello() -- "hello"
// printHello() -- "hello"
// printHello() -- "hello"
// printHello() -- "hello"
// printHello() -- "hello"
// ----
// call stack

然后,我们收到调用堆栈大小错误:

call-stack-error-1

最大调用堆栈大小超过错误

为了避免这种最大化调用堆栈的无限递归,我们需要一个停止递归的条件。

递归中的普通案例和基本案例

递归中的一般情况(也称为递归情况)是导致函数继续恢复(自称呼)的情况。

递归中的基数情况是递归函数的停止点。它是您指定停止递归的条件(就像停止循环一样)。

这里有一个例子:

let counter = 0

function printHello() {
  console.log("hello")
  counter++
  console.log(counter)

  if (counter > 3) {
    return;
  }

  printHello()
}

printHello()

在这里,我们的一般情况没有显式陈述,而是隐含的:如果计数器变量不大于3,函数应该继续调用自己

虽然明确说明的基本情况是,如果计数器变量大于3,则函数应该结束执行。这种情况将导致调用堆栈上的所有递归函数弹出,因为递归已结束。

这就是第一次调用printHello()时调用堆栈的样子:

// printHello()
// ----
// call stack

然后记录“你好”,counter变量增加1(使其成为1),counter变量也被记录。检查基本情况。“counter不大于3”,因此尚未满足条件。

该函数中的下一行是printHello()和调用堆栈中:

// printHello()
// printHello() -- "hello" -- 1
// ----
// call stack

“你好”再次被记录,counter变量被递增并记录。基本情况不符合“counter仍然不大于3”。然后,调用第二个函数中的printHello(),调用堆栈如下所示:

// printHello()
// printHello() -- "hello" -- 2
// printHello() -- "hello" -- 1
// ----
// call stack

相同的循环发生,再次调用printHello()

// printHello()
// printHello() -- "hello" -- 3
// printHello() -- "hello" -- 2
// printHello() -- "hello" -- 1
// ----
// call stack

将“hello”记录到控制台后,counter将增加1(使其成为4)。“4大于3”,这符合我们的基本情况,因此执行了return语句。

我们返回什么并不重要,但return会停止函数的执行。这意味着我们调用堆栈中的第四个printHello()将无法再次调用printHello(),因为无法到达该行。

接下来发生的事情是,第四个printHello()在完成执行后从调用堆栈中弹出:

// printHello() -- "hello" -- 3
// printHello() -- "hello" -- 2
// printHello() -- "hello" -- 1
// ----
// call stack

对于第三个printHello(),在它调用自己的行之后,函数中没有任何东西可以执行。因此,这意味着第三个printHello()也已完成其执行,并将从调用堆栈中弹出:

// printHello() -- "hello" -- 2
// printHello() -- "hello" -- 1
// ----
// call stack

第二次和第一次printHello()也是如此,从而使调用堆栈为空:

// ----
// call stack

因此,您看到我们如何通过提供基本情况来避免无限递归。递归函数应该至少有一个基本情况(您可以想拥有多少就有多少),以确保递归不会永远运行。

您可以通过不同的方式编写基本案例。这里有一个,一般情况是显式的,而基本情况是隐式的:

let counter = 0

function printHello() {
  console.log("hello")
  counter++
  console.log(counter)

  if (counter < 4) {
      printHello()
  }

  return;
}

printHello()

在这里,我们有一般情况,告诉函数继续恢复。这里的情况是,如果计数器小于4。 因此,如果这种情况得到满足,递归会继续发生。

但不像前一个例子那样明确的基本情况是,如果计数器不再小于4, 请转到下一行。这执行return,函数结束。然后,当他们完成执行时,调用堆栈上的所有内容都开始弹出。

总结

递归并不完全取代循环。但在某些情况下,递归可以更有效,用更少的代码行来更容易阅读。