尾调用
Tail Call 指某个函数的最后一步是调用另一个函数。
// 函数f 的最后一步是调用函数g
function f(x) {
return g(x)
}
// 不属于尾调用的情况
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
}
// 尾调用不一定出现在函数尾部,只要是最后一步操作即可:
// 函数m 和n 都属于尾调用
function f(x) {
if (x > 0) {
return m(x)
}
return n(x)
}
尾调用优化
函数调用会在内存形成一个“调用记录”,又称“调用帧”,保存调用位置和内部变量等信息。
如果在函数A 的内部调用函数B,那么在A 的调用帧上方还会形成一个B 的调用帧。等到B 运行结束,将结果返回到A,B 的调用帧才会消失。如果函数B 内部还调用函数C ,那就还有一个C 的调用帧。所有的调用帧形成一个“调用栈(call stack)”。
使用尾调用的话,因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。
尾调用优化的条件就是确定外部栈帧真的没有必要存在了,涉及条件如下:
- 代码在严格模式下执行:在非严格模式下函数调用中允许使用f.arguments 和f.caller,而它们都会引用外部函数的栈帧。
- 外部函数的返回值是对尾调用函数的调用
- 尾调用函数返回后不需要执行额外的逻辑
- 尾调用函数不是引用外部函数作用域中自由变量的闭包
"use strict"
// 无优化:尾调用没有返回
function outerFunction() {
innerFunction()
}
// 无优化:尾调用没有直接返回
function outerFunction() {
let innerFunctionResult = innerFunction()
return innerFunctionResult
}
// 无优化:尾调用返回后必须转型为字符串
function outerFunction() {
return innerFunction().toString()
}
// 无优化:尾调用是一个闭包
function outerFunction() {
let foo = 'bar'
function innerFunction() {
return foo
}
return innerFunction()
}
尾递归
函数调用自身称为递归,如果尾调用自身就称为尾递归。
递归非常耗费内存,因为需要同时保存成百上千个调用帧,很容易发生栈溢出错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生栈溢出错误。
// 计算n 的阶乘,最多需要保存n 个调用记录,复杂度为O(n)
function factorial(n) {
if (n === 1) { return 1}
return n * factorial(n - 1)
}
// 改为尾递归,只保留一个调用记录,复杂度为O(1)
function factorial(n, total) {
if (n === 1) { return total}
return factorial(n - 1, n * total)
}
// 非尾递归的Fibonacci 数列
function Fibonacci() {
if (n <= 1) {return 1}
return Fibonacci(n - 1) + Fibonacci(n - 2)
}
Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
Fibonacci(500) // 堆栈溢出
// 尾递归优化
function Fibonacci2(n, ac1 = 1, ac2 = 1) {
if (n <= 1) {return ac2}
return Fibonacci2(n - 1, ac2, ac1 + ac2)
}
Fibonacci2(100) // 573147844013817200000
非尾递归的Fibonacci 数列,栈帧数的内存复杂度是O(2n)
递归函数的改写
尾递归的实现往往需要改写递归函数,确保最后一步只调用自身,也就是把所有用到的内部变量改写成函数的参数,但是这样做的缺点是不太直观(参数太多,费解)。
// 法1 在尾递归函数之外再提供一个正常形式的函数
function tailFactorial(n, total) {
if (n === 1) { return total }
return tailFactorial(n - 1, n * total)
}
function factorial(n) {
return tailFactorial(n, 1)
}
factorial(5) // 120
// 法2 柯里化(currying),将多参数的函数转换成单参数形式
function currying(fn, n) {
return function(m) {
return fn.call(this, m, n)
}
}
function tailFactorial(n, total) {
if (n === 1) { return total }
return tailFactorial(n - 1, n * total)
}
const factorial = currying(tailFactorial, 1)
factorial(5) // 120
// 通过柯里化将尾递归函数tailFactorial 变为只接受1个参数的factorial。
// 法3 ES6 的函数默认值
function factorial(n, total = 1) {
if (n === 1) { return total }
return factorial(n - 1, n * total)
}
factorial(5) // 120