Bun、JavaScript 和 TCO

JavaScript 中的尾调用优化——尤其是 ECMAScript 规范要求的“正确的尾调用”——再次受到关注,因为 Bun(通过 JavaScriptCore)支持它,而 V8 和 SpiderMonkey 等主流引擎大多不支持。评论者围绕其在递归密集或函数式风格中的实际好处(如避免栈溢出、CPS、状态机)与其缺点展开讨论,例如调试更困难、栈语义改变,以及与 JS 数组操作和 mutation 结合时表现不佳。讨论还延伸到更广泛的问题:JavaScript 是否应更深入拥抱函数式范式、引擎在改变可观察行为时应在多大程度上遵循规范,以及那些在 Bun 中可运行但在 Node 或 Deno 中会失败或变慢的代码风险。

“TCO”的含义以及对标题的预期

  • 在这个语境中,TCO = 尾调用优化(或“正确的尾调用”),而不是总体拥有成本或其他缩写。
  • 一些读者起初以为这是一篇关于 Bun/JS 总体拥有成本的文章,因此略感失望,但仍觉得其中关于函数式的角度很有意思。

递归、TCO 与 JavaScript 语义

  • 尾调用优化被描述为不只是一个“优化”:当它有保证时,会通过阻止尾调用导致的栈增长来改变语言语义。
  • ECMAScript 规范在 strict mode 中要求正确的尾调用,但大多数主流引擎(V8、SpiderMonkey)都忽略了这一部分。WebKit/JSC 以及 Bun(使用 JSC)确实实现了它们。
  • 有人认为缺少 TCO 限制了表达能力(例如 CPS、互相递归函数、用尾调用函数编码的状态机)。

主流 JS 引擎为何避免 PTC

  • 主要提到的问题是开发者体验。TCO 会省略栈帧,使堆栈跟踪、调试器和错误报告工具不再忠实反映实际调用结构。
  • 也提到了安全性/realm 边界方面的复杂性以及实现复杂度。
  • 另一些人反驳说,调试器可以维护影子栈或元数据;Scheme 和 Lua 说明这类问题是可解决的。
  • 也有人认为这种抵触在某种程度上带有“政治”色彩,或者对函数式有偏见,而不纯粹是技术问题。

性能与代码风格争论

  • 这里有一个重要区分:
    • 仅仅是尾递归但分配开销很大的代码(例如每次调用都用 spread 构造新数组),即使有 TCO,渐进上也很糟。
    • 命令式循环或基于 mutation 的版本,在 JS 引擎里通常更快,也更节省内存。
  • 许多参与者觉得递归/三元运算的示例和一个简单循环相比很难读;有人称其为“聪明但难以辨认”。
  • 也有人为函数式风格辩护,认为对习惯它的人来说它完全可读,但也承认 JS 的数组和 GC 使天真的函数式模式效率很差。

TCO 在 JS 中的适用范围与实用性

  • 有几位认为 TCO 对纯函数式语言至关重要,但对于一种多范式、可变的语言来说没那么关键,因为循环才是惯用写法。
  • 担忧点在于:依赖 Bun/JSC TCO 的代码在 Node/Deno 中可能会栈溢出;因此建议加运行时检查或注释。
  • 还提到了 Python 刻意不支持 TCO(理由类似:调试/可读性)作为类比。

与 Bun 和引擎相关的说明

  • Bun 通过一个特殊的 $vm 接口暴露了用于尾调用的 JSC 内建能力,但误用可能导致进程崩溃。
  • Bun 因速度受到称赞(例如依赖安装);它与 Node/Next.js app router 的运行时一致性仍不完整,但已列入路线图。
  • 对于 AWS Lambda,Bun 可以通过自定义运行时/容器使用,但没有类似 Node 的冷启动优化。