before
先看一个JavaScript中常见的错误
栈溢出产生的原因
当函数递归调用自身或其他函数过于频繁,而递归调用的层次没有得到正确的终止条件,就可能导致栈溢出。
每次调用一个函数,JavaScript 引擎都会创建一个新的执行上下文,并将其推入调用栈(call stack)。调用栈是一个存储函数调用的栈结构,用于跟踪代码的执行顺序。当一个函数调用另一个函数时,新的执行上下文被推入栈中,当函数执行完成后,执行上下文从栈中弹出。
如果递归调用的层次过深,并且没有正确的终止条件,调用栈会不断增长,最终导致栈溢出。在这种情况下,调用栈的空间被用尽,无法再容纳新的执行上下文。
尾递归是个啥
尾递归是一种特殊形式的递归,指的是递归函数在执行的过程中,递归调用是整个函数体中最后执行的语句。在JavaScript中,尾递归对于优化函数的性能和避免栈溢出很有用。
在尾递归优化中,当前函数调用的返回值直接被传递给下一次递归调用,而不涉及额外的操作。这使得 JavaScript 引擎可以优化,将递归调用的栈帧重用,而不是不断地增加新的栈帧。
尾递归=尾调用+递归
尾调用(tail call)指的是一个函数的最后一条语句是一个返回调用函数的语句.
// 非尾递归,最后一条指令是加法运算 n + xxx,所以不是尾调用
function calc(n, count = 0) {
if (n === 0) return count;
return n + calc(n - 1, count)
}
// 尾递归,最后一条指令返回了函数调用,所以这是尾调用
function calc(count, total = 0) {
if (count === 1) return total;
return calc(count - 1, total + count)
}
ES6中的规定
在 ECMAScript 2015(ES6)标准中,引入了对尾调用优化(Tail Call Optimization,TCO)的规范(需要在严格模式下),传送门:262.ecma-international.org/6.0/#sec-ta…
尾递归与JavaScript执行引擎
所以,尾递归是否能够成功优化取决于具体的 JavaScript 引擎,不同的引擎在对尾递归的处理上可能有差异。
测试方法, 复制以下代码在不同JavaScript环境下执行
'use strict'
function testTCO(enabled) {
function factorial(n, acc = 1) {
if (n <= 1) return acc;
if (enabled) {
// 尝试使用尾调用优化
return factorial(n - 1, n * acc);
} else {
// 正常的递归调用
return n * factorial(n - 1, acc);
}
}
try {
// 尝试一个大的数字,通常足以导致堆栈溢出
console.log(factorial(100000));
return true;
} catch (e) {
console.log('Stack overflow or other error:', e);
return false;
}
}
console.log('Testing with potential TCO:', testTCO(true));
console.log('Testing without TCO:', testTCO(false));
以下是不同的环境下测试的结果
| 宿主 | 尾递归优化 |
|---|---|
| chrome v120 | ❌ |
| edge v121 | ❌ |
| nodejs v18.16 | ❌ |
| firefox v109 | ❌ |
| safari v15.4 | ✅ |
结论:目前主流浏览器只有Safari浏览器支持尾递归优化, 其他一概不支持, 所以目前来说尾递归实际意义不大,但是没准以后会有用