返回

文章详情

递归在欺骗你

Hacker News2026年7月28日 20:20

出现在 Node Weekly #624 和 Javascript Weekly - 2026-06-02 * 递归是开发者早期学习并信任的概念之一。如果递归步骤简单且基本情况正确,代码看起来干净且安全。它的优雅有其原因:许多问题天然具有递归性,代码往往反映出我们口头上解释逻辑的方式。对于树遍历、嵌套结构和分治模式,递归比显式循环更易阅读。关键在于物理限制。即使有正确的基本情况和合理的逻辑,每次递归调用仍然会消耗栈空间。在某一深度时,你会因为栈溢出而崩溃。如果你阅读过《你的防抖在欺骗你》和《你的节流在欺骗你》,这就是同一模式的递归版本:优雅的抽象,隐藏的操作边缘。甚至依赖管理也可能在欺骗你,这在《你的包管理器在欺骗你》中有所探讨。对于另一类静默失败,《你的 JS 日期在欺骗你》涵盖了 JavaScript 日期 API 中内置的解析、变更和时区陷阱。问题设置:递归撞墙 你可以直接在浏览器控制台运行下面的所有内容。让我们先从简单的开始:一个从 1 到 n 的递归求和。 function sum ( n ) { if ( n === 0 ) return 0 ; return n + sum ( n - 1 ) ; } sum ( 10 ) ; // 55 现在推送一个大输入:sum ( 100000 ) ; // 在大多数 JS 运行时中会抛出 RangeError 或 InternalError:递归过多 发生了什么?这个函数在逻辑上是正确的,但是每次对 sum 的调用停留在栈上,直到下面的调用返回。在深度 100,000 时,运行时耗尽了栈空间并抛出。与结果错误无关,纯粹是物理限制,运行时一次最多能保存多少个嵌套帧。尾递归救援故事 通常的下一步是尾调用优化。这个想法很简单:让递归调用成为函数最后执行的事情,这样运行时就可以重用同一帧而不是推送一个新帧。注意,尽管递归调用出现在最后一行,但 sum 不是尾递归。在 sum(n - 1) 返回后,仍待处理的工作:结果必须加到 n 上。只有当它的返回值被立即转发且没有后续计算时,调用才在尾位置。尾递归版本将待处理状态移入累加器: function sumTR ( n , acc = 0 ) { if ( n === 0 ) return acc ; return sumTR ( n - 1 , acc + n ) ; } sumTR ( 10 ) ; // 55 在这里,sumTR(...) 是最后发生的事情——没有待处理的加法,没有待处理的任何事情。运行总数在 acc 中,而不在等待的栈帧中。从理论上讲,实现 TCO 的运行时能够在常量栈空间中执行此操作,而不管深度如何。现在重复相同的压力输入:sumTR ( 100000 ) ; // 可能仍会抛出 RangeError!即使有正确的尾递归结构,许多 JavaScript 运行时仍会每次调用分配一个新的栈帧,并在大深度时抛出。这让期望 TCO 是全局保证的开发者感到惊讶。ECMAScript 2015 在严格模式中正式指定了适当的尾调用,但大多数引擎从未一致地采用这一特性。某些引擎实现了这一特性,然后由于性能回退而撤回。其他引擎则根本没有实现。结果是,你无法假定尾递归在生产 JavaScript 中是栈安全的,即使代码结构正确以支持 TCO。关于斐波那契的说明 斐波那契是典型的递归教科书示例,它也会遇到栈限制,但它还带有一个更严重的问题:指数时间复杂度。 function fib ( n ) { if ( n <= 1 ) return n ; return fib ( n - 1 ) + fib ( n - 2 ) ; } 每个调用分支出两个更多的调用,因此调用的总数以 O(2ⁿ) 增长。fib(30) 已经产生超过一百万次调用;fib(50) 的调用达到数百亿次。在浏览器中,这早在达到任何栈限制之前就会使标签页冻结,这使得失败模式看起来与栈溢出完全相同,但根本原因却完全不同。斐波那契的尾递归版本: function fibTR ( n , a = 0 , b = 1 ) { if ( n === 0 ) return a ; if ( n === 1 ) return b ; return fibTR ( n - 1 , b , a + b ) ; } 这一版本的运行时间是线性时间,但在大 n 时仍然面临栈溢出的风险,因为 TCO 的不确定性。指数版本在这个讨论中是一个红鲱鱼,因为它由于完全不同的原因而失败:栈溢出和指数膨胀是两个独立的问题。它们从外部看起来相同(页面挂起或崩溃),但是需要完全不同的修复。运行时现实(截至写作时) 截至写作时(2026 年 5 月),在 JavaScript 运行时中,无法依赖适当的尾调用优化支持。 运行时引擎 可靠的适当尾调用? 实际情况 Chrome V8 否 不要期望栈安全的尾递归。 Node.js V8 否 尾递归代码仍然可能溢出。 Deno V8 否 相同的操作限制

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡