一、什么是尾调用?

[!NOTE] 尾调用的概念非常简单,一句话就能说清楚:即某个函数的最后一步是调用另一个函数。

function f(x){
  return g(x);
}

上面代码中,函数 f 的最后一步是调用函数 g,这就叫尾调用。

不属于尾调用的情况

以下两种情况,都不属于尾调用:

情况一

function f(x){
  let y = g(x);
  return y;
}

在这种情况下,调用函数 g 之后,还有其他操作(将 g(x) 的结果赋值给变量 y),所以不属于尾调用。

情况二

function f(x){
  return g(x) + 1;
}

即使语法上看起来是尾调用,但因为返回结果是 g(x) + 1,还进行了额外的操作,所以也不属于尾调用。

尾调用的其他情况

尾调用不一定出现在函数尾部,只要是最后一步操作即可。例如:

function f(x) {
  if (x > 0) {
    return m(x);
  }
  return n(x);
}

上面代码中,函数 mn 都属于尾调用,因为它们是函数 f 的最后一步操作。

二、尾调用优化

尾调用与其他调用的不同之处在于它的位置。我们知道,==函数调用会在内存中形成一个“调用记录”(call frame),保存调用位置和内部变量等信息。==如果在函数 A 内部调用函数 B,那么在 A 的调用记录上方,会形成一个 B 的调用记录。等到 B 运行结束,将结果返回到 A,B 的调用记录才会消失。如果函数 B 内部还调用函数 C,那就会有一个 C 的调用记录栈,以此类推,所有的调用记录就会形成一个“调用栈”(call stack)。

尾调用的优化

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到。只要直接用内层函数的调用记录替代外层函数的调用记录,就可以了。

function f() {
  let m = 1;
  let n = 2;
  return g(m + n);
}
f();

// 等同于
function f() {
  return g(3);
}
f();

// 等同于
g(3);

在上面的代码中,如果函数 g 不是尾调用,函数 f 就需要保存内部变量 mn 的值,以及 g 的调用位置等信息。但由于调用 g 之后,函数 f 就结束了,所以执行到最后一步时,可以完全删除 f() 的调用记录,只保留 g(3) 的调用记录。

这就是“尾调用优化”(Tail Call Optimization,TCO),即只保留内层函数的调用记录。如果所有函数都是尾调用,那么在每次执行时,调用栈中只有一项记录,这将大大节省内存。这也是尾调用优化的意义所在。

尾递归

[!TIP] 尾递归就很简单就是尾调用不调用其他函数,而是调用自己,就是尾递归了.

[!WARNING] 尾递归就是一个对递归而言是优化的东西,但是尾递归能否发挥他的优化作用其实也要看这个编译器是否支持对他优化 (尾递归优化). 是将递归的调用转换成迭代. 因此并不是所有编程语言都支持尾递归优化