什么是尾调用

  • 尾调用(Tail Call)是函数式编程的一个重要概念,就是指某个函数的最后一步是调用另一个函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 尾调用
function f(x){
return g(x);
}
// 不属于尾调用, 调用函数g之后,还有赋值操作
function f(x){
let y = g(x);
return y;
}

// 不属于尾调用, 调用后还有操作,即使写在一行内
function f(x){
return g(x) + 1;
}

// 不属于尾调用,下面的两个函数等同
function f(x){
g(x);
}
function f(x){
g(x);
return undefined;
}

// 尾调用, 虽然尾调用没出现在函数尾部,但是只要是最后一步操作即可
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}

尾调用优化

  • 函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。
  • 尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
  • 只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。
1
2
3
4
5
6
7
8
// 不会进行尾调用优化,因为内层函数inner用到了外层函数addOne的内部变量one。
function addOne(a){
var one = 1;
function inner(b){
return b + one;
}
return inner(a);
}

尾递归

  • 函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
  • 递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
1
2
3
4
5
6
7
8
9
10
11
// 正常递归,复杂度 O(n)
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}

// 尾递归,复杂度 O(1)
function factorial(n, total = 1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}

来源: http://es6.ruanyifeng.com/#docs/function#尾调用优化