在javascript中的尾递归

时间:2015-02-28 10:26:56

标签: javascript recursion tail-recursion

我在JS方面不是很有经验。但是,作为大多数人,我有时需要在浏览器中添加一些额外的功能。

在寻找我遇到的其他问题的答案时this answer at SO.答案和the responder被高度评价,而且根据SO标准,这意味着它们非常可靠。 引起我注意的是,在“neatened up”变体中,它使用尾递归来循环函数:

(function myLoop (i) {          
   setTimeout(function () {   
      alert('hello');          //  your code here                
      if (--i) myLoop(i);      //  decrement i and call myLoop again if i > 0
   }, 3000)
})(10);     

从我的角度来看,这看起来像是糟糕的工程。使用递归来解决命令式/ OO语言中的非递归问题就是要求麻烦。十次或100次迭代应该是安全的。但是10000或无限循环呢? 在像Erlang和Haskell这样的纯函数式语言中,我知道尾递归在编译期间被转换为循环,并且不会向堆栈添加额外的帧。据我所知,对于例如C / C ++或Java的所有编译器都不是这样。

JS怎么样?在没有SO风险的情况下使用尾递归是否安全?或者这取决于脚本运行的实际解释器?

4 个答案:

答案 0 :(得分:8)

您提供的示例没有任何尾递归。考虑:

(function loop(i) {
    setTimeout(function main() {
        alert("Hello World!");
        if (i > 1) loop(i - 1);
    }, 3000);
}(3));

  1. 我给内部函数命名为main,外部函数命名为loop
  2. 立即使用值loop调用3函数。
  3. loop函数只做一件事。它调用setTimeout然后返回。
  4. 因此,对setTimeout的调用是尾调用。
  5. 现在,在main毫秒之后,JavaScript事件循环会调用3000
  6. 调用main后,loopsetTimeout都已完成执行。
  7. main函数有条件地调用loop,其值为i
  8. main调用loop时,这是一个尾调用。
  9. 但是,无论是递归100次还是10000次都没关系,堆栈大小永远不会增加太多而导致溢出。原因是当您使用setTimeout时,loop函数会立即返回。因此,当main被称为loop时,不再是堆栈。
  10. 直观的解释:

    |---------------+ loop (i = 3)
                    |---------------+ setTimeout (main, 3000)
                                    |
                    |---------------+ setTimeout return
    |---------------+ loop return
    ~
    ~ 3000 milliseconds
    ~
    |---------------+ main (i = 3)
                    |---------------+ alert ("Hello World!")
                                    |
                    |---------------+ alert return
                    | i > 1 === true
                    |---------------+ loop (i = 2)
                                    |---------------+ setTimeout (main, 3000)
                                                    |
                                    |---------------+ setTimeout return
                    |---------------+ loop return
    |---------------+ main return
    ~
    ~ 3000 milliseconds
    ~
    |---------------+ main (i = 2)
                    |---------------+ alert ("Hello World!")
                                    |
                    |---------------+ alert return
                    | i > 1 === true
                    |---------------+ loop (i = 1)
                                    |---------------+ setTimeout (main, 3000)
                                                    |
                                    |---------------+ setTimeout return
                    |---------------+ loop return
    |---------------+ main return
    ~
    ~ 3000 milliseconds
    ~
    |---------------+ main (i = 1)
                    |---------------+ alert ("Hello World!")
                                    |
                    |---------------+ alert return
                    | i > 1 === false
    |---------------+ main return
    

    以下是发生的事情:

    1. 首先,调用loop(3)并在调用3000后调用main毫秒。
    2. 再次调用main函数调用loop(2)并返回3000main毫秒。
    3. 再次调用main函数调用loop(1)并返回3000main毫秒。
    4. 因此,由于setTimeout,堆栈大小永远不会无限增长。

      阅读以下问题和答案以获取更多详细信息:

      What's the difference between a continuation and a callback?

      希望有所帮助。

      P.S。尾部调用优化将在ECMAScript 6(Harmony)中引入JavaScript,它可能是列表中最期待的功能。

答案 1 :(得分:5)

该代码本身不是递归的,恰恰相反,它使用continuation passing来消除尾调用。这是一个没有setTimeout的例子:

// naive, direct recursion

function sum_naive(n) {
  return n == 0 ? 0 : n + sum_naive(n-1);
}

try {
  sum_naive(50000)
} catch(e) {
  document.write(e + "<br>")
}


// use CPS to optimize tail recursive calls

function sum_smart(n) {
  
  function f(s, n) {
    return n == 0 ? s : function() { return f(s+n, n-1) };
  }
  
  var p = f(0, n)
  
  while(typeof p == "function")
    p = p()
    
  return p;
}

document.write(sum_smart(50000) + "<br>")

CPS通常用于不支持开箱即用的语言中的尾递归优化。 Javascript的setTimeout基本上采用当前的延续并将其“抛出”到主线程。一旦主线程准备就绪,它“捕获”延续并在恢复的上下文中运行代码。

答案 2 :(得分:1)

这不是一个明确的递归。 myLoop的每次调用都将在另一个执行堆栈上执行(有点像一个单独的线程),并且不依赖于先前的调用。如原始答案:

  

setTimeout()函数是非阻塞的,将立即返回。

有一个myLoop函数,用于启动超时和匿名函数,用于处理超时后应执行的操作。由myLoop()(将为undefined)返回的值不会在稍后的调用中使用。

答案 3 :(得分:0)

当前,大多数JS运行时不支持尾部递归。因此,除非您确切知道代码将在哪个运行时运行,否则依靠尾部递归来避免“超出最大调用堆栈大小”错误是不安全的。

它是not supported in Node(版本6.4和<8除外,可以通过标记启用它)。

Safari 11和12版本似乎也支持它,但是no other major browsers do

博士Axel Rauschmayer在他的博客2ality on 2018-05-09中提到,永远不会得到广泛的支持。