一、什么是尾递归优化?
在递归调用中,如果函数的最后一步操作是调用另一个函数 (包括它自身),并且直接返回这个调用的结果 ,那么这个调用就是尾调用(Tail Call) 。
当尾调用的对象是自身时,就叫尾递归(Tail Recursion) 。
尾递归优化(Tail Call Optimization, TCO)的原理是:
在尾调用时,当前函数的栈帧不再需要保留,因此可以复用栈帧 ,避免深递归导致的栈溢出 (RangeError: Maximum call stack size exceeded
)。
二、规范与现实
规范层面
ES6 规范要求:
- 仅在 严格模式 (
'use strict'
)下启用尾调用优化。 - 仅对直接尾调用(Direct Tail Call)生效。
实现层面
现实是------目前(2025 年)大多数主流 JS 引擎(V8 / SpiderMonkey / JavaScriptCore)并未实际启用 TCO 。
因此,即使写成尾递归形式,也不会自动优化,深度递归仍会导致栈溢出。
三、尾递归优化的条件
一个调用要被视为"可优化的尾调用",必须同时满足:
- 严格模式 :函数必须处于
'use strict'
模式。 - 直接返回:调用结果直接作为当前函数的返回值。
- 调用是最后一步:调用之后没有额外运算或逻辑。
- 没有被包裹:调用不能被其它表达式或函数包装。
- 调用在当前作用域内执行:不能依赖外层执行环境的未完成逻辑。
四、正确与错误示例
✅ 正确示例 1:累加器模式
js
'use strict';
function sum(n, acc = 0) {
if (n <= 0) return acc;
return sum(n - 1, acc + n); // 直接返回尾调用
}
console.log(sum(5)); // 15
❌ 错误示例 1:尾调用后还有运算
js
'use strict';
function sum(n) {
if (n <= 0) return 0;
return 1 + sum(n - 1); // ❌ 尾调用结果还参与加法运算
}
原因:调用结束后还需要做 + 1
运算,不是纯尾调用。
❌ 错误示例 2:被包裹在函数中
js
'use strict';
function sum(n, acc = 0) {
if (n <= 0) return acc;
return (function(x) { return x; })(sum(n - 1, acc + n)); // ❌ 被包裹
}
原因:尾调用的返回值先传入另一个函数中,不是直接返回。
❌ 错误示例 3:非严格模式
js
function sum(n, acc = 0) { // ❌ 没有 'use strict'
if (n <= 0) return acc;
return sum(n - 1, acc + n);
}
原因:规范要求仅在严格模式下才可能触发优化。
五、现实中的替代方案
由于大部分引擎没有实际 TCO,如果要处理深递归,可以:
1. 改写为循环
js
function sumLoop(n) {
let acc = 0;
while (n > 0) {
acc += n;
n--;
}
return acc;
}
2. 蹦床(Trampoline)技术
js
function trampoline(fn) {
return function(...args) {
let result = fn(...args);
while (typeof result === 'function') {
result = result();
}
return result;
};
}
function sumT(n, acc = 0) {
if (n <= 0) return acc;
return () => sumT(n - 1, acc + n);
}
const safeSum = trampoline(sumT);
console.log(safeSum(1000000)); // 不会栈溢出
六、总结
- 尾递归优化是 JS 规范中的特性,但目前几乎没有引擎实现。
- 符合尾调用条件需要 严格模式 + 直接返回 + 最后一行调用。
- 想要在实际环境避免栈溢出,最好用循环 或蹦床函数手动实现。
- 尾递归写法依然有价值,因为它可以让逻辑更清晰,并且未来一旦引擎普及 TCO,你的代码可以无缝获益。