尾调用优化

一、什么是尾调用(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被压入栈,xy依次被创建并赋值,栈内也会记录相应的信息,同时也记录了该函数被调用的地方,这样在函数 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)); // 不会栈溢出

  

posted @   我是格鲁特  阅读(213)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示