一、尾调用
尾调用(Tail Call)
是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。
1 | // 函数f的最后一步是调用函数g,这就叫尾调用 |
二、尾调用优化
函数调用会在内存形成一个调用帧(call frame)
,保存调用位置和内部变量等信息。所有的调用帧
,就形成一个调用栈(call stack)
。
- 如果在函数
A
的内部调用函数B
,那么在A
的调用帧上方,还会形成一个B
的调用帧。等到B
运行结束,将结果返回到A
,B
的调用帧才会消失。
尾调用
由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
注意:只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行尾调用优化
。
1 | // 不会进行尾调用优化,因为内层函数inner用到了外层函数addOne的内部变量one |
三、尾递归
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生栈溢出
错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生栈溢出
错误。
1 | // 计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) |
1 | // 非尾递归的 Fibonacci 数列 |
注:ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。(验证:结论错误,Fibonacci2是正常模式)
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。
func.arguments
:返回调用时函数的参数。func.caller
:返回调用当前函数的那个函数。
尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。