尾调用与尾递归

91 阅读3分钟

尾调用定义

  • 尾调用指的是函数的最后一步调用另一个函数
function f(x) {
    return g(x)
}
  • 尾调用必须满足两个条件
    • 函数的最后一步是return另一个函数(如上述例子),这里和闭包有点像
    • return 后面的表达式必须仅仅是某个函数的调用,除此之外不能包含其它任何别的操作

尾调用优化

  • 代码执行是基于执行栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化
  • ES6的尾调用优化只在严格模式下开启,正常模式是无效的

尾递归定义

  • 在一个尾调用中,如果函数最后的尾调用位置上是这个函数本身,则被称为尾递归
  • 递归很常用,但如果没写好的话也会非常消耗内存,导致爆栈
  • 非尾递归的斐波拉契数列递归写法
function fibonacci(n) { 
 if (n === 0) return 0
 if (n === 1) return 1
 return fibonacci(n - 1) + fibonacci(n - 2)
}
  • 当n=5时,调用栈为
[fibonacci(5)]
[fibonacci(4) + fibonacci(3)]
[(fibonacci(3) + fibonacci(2)) + (fibonacci(2) + fibonacci(1))]
[((fibonacci(2) + fibonacci(1)) + (fibonacci(1) + fibonacci(0))) + ((fibonacci(1) + fibonacci(0)) + fibonacci(1))]
[fibonacci(1) + fibonacci(0) + fibonacci(1) + fibonacci(1) + fibonacci(0) + fibonacci(1) + fibonacci(0) + fibonacci(1)]
[1 + 0 + 1 + 1 + 0 + 1 + 0 + 1]
5 
  • 第 5 项调用栈长度就有 8 了,一些复杂点的递归稍不注意就会超出限度,同时也会消耗大量内存。而如果用尾递归的方式来优化这个过程,就可以避免这个问题
function fibonacciTail(n, a = 0, b = 1) { 
 if (n === 0) return a
 return fibonacciTail(n - 1, b, a + b)
}
fibonacciTail(5) === fibonacciTail(5, 0, 1) 
fibonacciTail(4, 1, 1) 
fibonacciTail(3, 1, 2) 
fibonacciTail(2, 2, 3) 
fibonacciTail(1, 3, 5) 
fibonacciTail(0, 5, 8) => return 5
  • 使用尾递归实现斐波拉契数列,每次递归都不会增加调用栈的长度,只是更新当前的堆栈帧而已。也就避免了内存的浪费和爆栈的危险

尾递归优化

  • 尾调用在没有进行任何优化的时候和其他的递归方式一样,该产生的调用栈一样会产生,一样会有爆栈的危险。而尾递归之所以可以优化,是因为每次递归调用的时候,当前作用域中的局部变量都没有用了,不需要层层增加调用栈再在最后层层回收,当前的调用帧可以直接丢弃了,这才是尾调用可以优化的原因
  • 改写为循环进行优化
function fibonacciLoop(n, a = 0, b = 1) { 
 while (n--) {
 [a, b] = [b, a + b]
 }
 return a
}
  • 蹦床函数
    • 借助一个蹦床函数的帮助,它的原理是接受一个函数作为参数,在蹦床函数内部执行函数,如果函数的返回是也是一个函数,就继续执行
function trampoline(f) { 
 while (f && f instanceof Function) {
 f = f()
 }
 return f
}

-------------------------------------------------------------------------------------------2024.5.7每日一题