尾调用定义
- 尾调用指的是函数的最后一步调用另一个函数
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每日一题