译:Bun、JavaScript 和 TCO

译者注: 翻译前对 TCO(尾调用)不是很熟悉,于是借助 AI 先了解了一下基本知识。

在 JavaScript 中,尾调用是指一个函数在最后一步调用另一个函数。具体来说,这个调用发生在调用者的返回位置,且该调用的返回值直接被当前函数返回。

优点:

  • 节省内存:尾调用优化可以减少内存消耗,因为不需要保留当前函数的执行上下文.
  • 提高性能:优化尾调用可以减少函数调用的开销,提升程序的执行效率。

缺点:

  • 限制条件:尾调用优化有特定条件,不是所有情况都会被优化。
  • 复杂性:在某些情况下,使用尾调用可能导致代码逻辑变得复杂难以理解。

应用场景:

  • 递归函数:尾调用常用于优化递归函数,确保递归深度不会导致栈溢出。
  • 状态机:在状态机和状态转换过程中,尾调用可以简化控制流程。
  • 函数式编程:在函数式编程范式中,尾调用是一种常见的优化方式。

Bun 是一个刚发布了 1.0 版本的 JavaScript 运行时。现在你有了三个在浏览器之外运行 JavaScript 的选择:Node、Deno 和 Bun。Bun 的卖点之一就是其速度!为此,它做了一些有趣的决定。

其中之一便是 Bun 使用了 Zig 进行编写。这造就了一个令人兴奋的宇宙:Node 使用 C++,Deno 使用 Rust,Bun 使用 Zig。这难道不是一场激动人心的语言战争吗?!当然,我们会更关注其他事情。

Node 和 Deno 是基于 V8 引擎构建的,而 Bun 则基于 JavaScriptCore。你可能听说过 V8 是 Chrome 的 JavaScript 引擎。JavaScriptCore 则是 Safari 的引擎。它们有许多有趣的差异,不过我们将重点关注 JavaScriptCore 实现而 V8 尚未实现的优化:尾调用优化。

让我们写一些真实的代码来深入研究!想象一下你需要实现以下功能:

javascript 复制代码
/*
  Returns an array of numbers counting from 1 to amount.

  Examples:
    count(3) => [1, 2, 3]
    count(5) => [1, 2, 3, 4, 5]
    count(-1) => []
*/
function count(amount: number): number[];

你也可以自己尝试一下!我想大部分的人会这么实现:

javascript 复制代码
function count(amount: number): number[] {
    let nums: number[] = [];
    for (let i = 1; i <= amount; i++) nums.push(i);
    return nums;
}

这是一个很棒的实现,并且完全能运行!但现在,我会任性地提出一个挑战来引入尾调用优化。你可以将上述代码变为递归形式吗?试一试。一番思考之后,你可能会想到这样:

javascript 复制代码
const count = (amount: number) => (amount > 0 ? [...count(amount - 1), amount] : []);

这是个非常简洁的方法!非常像数学课上讲的递归关系。你可能会思考,"看来循环可以用递归来更优雅地表达!"但现在我要分享一个让人难过的消息。尝试执行count(100000)(Deno 和 Bun 允许直接运行 TypeScript)。你会得到报错Maximum call stack size exceeded

递归占用了调用栈上宝贵的内存!或许有命令可以增加程序的调用栈大小,但操作系统会进行限制,堆上的内容限制则会小一些。我们该怎样使用递归而不用担心栈溢出呢?答案是:希望你的 JavaScript 引擎能支持TCO(尾调用),并且可以优化你的递归。

重写函数利用TCO 步骤包含将状态变量转移到函数参数。递归调用必须是函数 AST 的最后一步。TCO 的版本如下:

javascript 复制代码
const count = (amount: number, cur: number[] = []) =>
  cur.length >= amount ? cur : count(amount, [...cur, cur.length + 1]);

它看上去不那么地简洁和优雅,但可以被尾调用优化了!如果我们使用 Deno 运行count(100000),我们仍然会得到错误error: Uncaught RangeError: Maximum call stack size exceeded。但使用 Bun 的话,程序就能成功运行了!但仍然有另一个问题......那就是运行速度太慢了。

count(100000) 在 Bun 中利用 TCO 的方法运行需要 7 秒。而原始的for循环方法只需.01s。我们要怎样才能让递归的性能接近for循环呢?我们可以使用引用类型(mutation)。

javascript 复制代码
function count(amount: number, cur: number[] = []) {
    if (cur.length >= amount) return cur;
  cur.push(cur.length + 1);
    return count(amount, cur);
}

这个方法看起来像原来 for 循环的方法。它即不简洁也不优雅。但它能在.01s跑完count(100000)。不错!

我心中极简主义的部分十分喜欢 TCO。它可以让一个语言不用循环就能完成复杂且高效的程序。就 JavaScript 而言,这意味着可以用更小的子集表达所有程序。大部分的初学者都会学习循环语句,就像是每种语言的基础或者必修。但事实并非如此。使用 TCO 可以用递归来实现循环并且有类似的性能。

像 LISP 这类以来递归的语言,在语言层面就实现了 TCO。但遗憾的是,TCO 仅在 JavaScriptCore 中实现了。不过感谢 Bun 和 Safari 使用了 JavaScriptCore!

相关推荐
anyup_前端梦工厂28 分钟前
Vuex 入门与实战
前端·javascript·vue.js
程序员阿鹏41 分钟前
ArrayList 与 LinkedList 的区别?
java·开发语言·后端·eclipse·intellij-idea
你挚爱的强哥1 小时前
【sgCreateCallAPIFunctionParam】自定义小工具:敏捷开发→调用接口方法参数生成工具
前端·javascript·vue.js
喝旺仔la1 小时前
Element 表格相关操作
javascript·vue.js·elementui
米老鼠的摩托车日记1 小时前
【vue element-ui】关于删除按钮的提示框,可一键复制
前端·javascript·vue.js
forwardMyLife1 小时前
element-plus的菜单组件el-menu
javascript·vue.js·elementui
java_heartLake2 小时前
微服务中间件之Nacos
后端·中间件·nacos·架构
好多吃的啊2 小时前
背景图鼠标放上去切换图片过渡效果
开发语言·javascript·ecmascript
Passion不晚2 小时前
打造民国风格炫酷个人网页:用HTML和CSS3传递民国风韵
javascript·html·css3
GoFly开发者2 小时前
GoFly快速开发框架/Go语言封装的图像相似性比较插件使用说明
开发语言·后端·golang