外观
什么是尾递归?
尾调用是函数在返回前做的最后一件事就是调用另一个函数,而且不再使用这次调用的结果做计算。尾递归是尾调用自己。严格模式下,引擎可以把这种调用复用成同一个栈帧,避免栈溢出。
原理
阶乘如果写成 return n * factorial(n - 1),乘法发生在递归返回之后,不是尾调用。改成把累乘放进参数:
js
function factorial(n, acc = 1) {
"use strict"
if (n <= 1) return acc
return factorial(n - 1, n * acc)
}最后一行只有递归调用,当前帧上的 n 和 acc 不再被需要。满足条件时,栈深度可以保持常数。
实际引擎支持不一致。需要稳定的深度时,不要依赖尾调用优化,改成循环,或者自己用栈模拟。递归的价值是树、目录这种结构写起来直接,终止条件必须写在递归调用之前。
追问
尾调用优化需要哪些条件?
严格模式、调用是返回语句里的最后动作、不能把调用包在别的表达式里。闭包如果还要引用当前帧的变量,这一帧就不能丢掉。
递归和循环怎么选?
数据是树、图、嵌套评论,递归更清晰,同时要限制深度或改成显式栈。扁平列表求和用循环或 reduce。
易错点
return n * f(n - 1)看起来在最后,仍然不是尾调用。- 非严格模式没有这套优化。
- 没有终止条件的递归会撑满执行栈,和尾不尾无关。