尾调用优化
一、什么是尾调用(Tail Call)
当一个函数调用另一个函数时,如果调用语句是该函数中的最后一步,并且返回该调用结果,这个调用就是尾调用。
1 2 3 | function foo() { return bar(); // bar()是foo()的最后一步调用,所以是尾调用 } |
以下两种情况均不属于尾调用,因为在函数调用后还有其他的操作:
1 2 3 4 5 6 7 8 9 10 | // 情况一 function f(x){ let y = g(x); return y; } // 情况二 function f(x){ return g(x) + 1; } |
二、调用栈及尾调用优化
在了解尾调用优化前,需要先理解调用栈,调用栈是解释器(比如浏览器中的 JavaScript 解释器)追踪函数执行流的一种机制。当执行环境中调用了多个函数时,通过这种机制,我们能够追踪到哪个函数正在执行,执行的函数体中又调用了哪个函数
- 每调用一个函数,解释器就会把该函数添加进调用栈并开始执行。
- 正在调用栈中执行的函数还调用了其它函数,那么新函数也将会被添加进调用栈,一旦这个函数被调用,便会立即执行。
- 当前函数执行完毕后,解释器将其清出调用栈,继续执行当前执行环境下的剩余的代码。
- 当分配的调用栈空间被占满时,会引发“堆栈溢出”错误。
1 2 3 4 5 6 7 8 9 10 11 | const fn1 = (a) => { let b = a + 1; return b; } const fn2 = (x) => { let y = x + 1; return fn1(y); // line A } const result = fn2(1); // line B |
在上面的代码中,首先fn2
被压入栈,x
、y
依次被创建并赋值,栈内也会记录相应的信息,同时也记录了该函数被调用的地方,这样在函数 return 后就能知道结果应该返回到哪里。
当fn2中执行到fn1时,fn1
入栈,当它运行结束后就可以出栈,之后fn2
也得到了想要的结果,返回结果后也出栈,此段代码运行结束,如下图所示:
观察上图,第2、3步骤中的fn2,
它内部的一切计算都已经完成了,此时它在栈内的唯一作用就是记录最后结果应该返回到哪一行。因而可以有如下的优化:
尾调用优化(TCO)会在函数尾部调用时,不再创建新的栈帧,而是复用当前的栈帧。这可以有效避免因递归深度过大而导致的栈溢出问题。在ES6的严格模式下,JavaScript引擎会对尾调用进行优化。
三、尾递归优化
递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。
下面是一段求N个数累加的函数,如果使用普通的递归函数如下:
1 2 3 4 | const sum = (n) => { if (n <= 1) return n; return n + sum(n-1) } |
改为尾递归调用后如下:
1 2 3 4 | const sum = (n,total=0) => { if (n<=1) return n+total; return sum(n-1,n+total) } |
尾调用优化的限制
- 必须在严格模式下使用。
- 尾调用必须作为函数的最后一步,并直接返回调用的结果,不能有额外的操作。
- 支持情况:并非所有的JavaScript引擎都完全支持尾调用优化。虽然ES6规范要求在严格模式下进行尾调用优化,但一些引擎(如V8)在默认实现中并未完全支持。
js引擎不支持时就要使用蹦床函数(Trampoline)来将递归转换为循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 | const sum = (n, total = 0) => { if (n <= 1) return n + total; return () => sum(n-1, n + total) } const trampoline = f => (...args) => { let result = f(...args); while ( typeof result === 'function' ) { result = result(); } return result; } const sum0 = trampoline(sum); console.log(sum0(1000000)); // 不会栈溢出 |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了