今天在复习栈和队列的时候,突然出现这么一个概念,虽然从表面看好像是一个简单地不能再简单的问题,但是上网一查却发现这个知识点被很多人所研究过,于是打算顺便总结一下!
什么是尾调用
顾名思义,就是指某个函数的最后一步是调用另一个函数。如以下代码中,1属于尾调用,2、3都不属于尾调用。
// 1
function f(x) {
return g(x);
}
// 2
function f(x) {
let y = g(x);
return y;
}
// 3
function f(x) {
return g(x) + 1;
}
尾调用优化
函数调用会形成调用帧,保存调用位置和内部变量等信息。若A调用B,则A上方会形成B的调用记录,所有记录构成调用栈。尾调用不需保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了,这叫做尾调用优化,将大大节省内存,具体可以参考以下代码:
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
尾递归
递归非常耗费内存,很容易发生栈溢出(stack overflow)。但尾递归由于只存在一个调用记录,所以不会栈溢出。
function factorial(n) {
if (n === 1)
return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
上面的函数最多需保存n个调用记录,复杂度O(n)。若改成尾递归,只需保留一个调用记录,复杂度O(1)。尾递归实现往往需要改递归函数,把所有用到的内部变量改写成函数参数,具体如下:
function factorial(n, total) {
if (n === 1)
return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120