【问题标题】:Tail recursion in javascriptjavascript中的尾递归
【发布时间】:2015-05-01 02:02:39
【问题描述】:

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

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

(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语言中使用递归解决非递归问题是自找麻烦。 10 或 100 次迭代应该是安全的。但是 10000 或无限循环呢? 在 Erlang 和 Haskell 等纯函数式语言中,我知道尾递归在编译期间会转换为循环,并且不会向堆栈添加额外的帧。据我所知,并非所有编译器都如此,例如 C/C++ 或 Java。

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

【问题讨论】:

  • 大部分选票来自这样一个事实,即这是一个很容易通过谷歌搜索的问题,而不是它特别好。代码不好,他们应该改用setInterval()
  • setTimeout 用于在调用后 x 毫秒调用您的函数。每 x 毫秒执行一次是一种常见的模式。在这种情况下,for 循环需要调用所有 setTimeout,每个都有一个已知的延迟:x * 3000。不知道它是否会导致 SO,因为它不是真正的递归。
  • @Haketo 为什么称其为“不是真正的递归”?您如何对此进行分类,为什么?对我来说,这看起来就像我将如何在 Erlang 中通过尾递归实现循环。
  • @EinarSundgren a setTimeout 在“主”执行堆栈上添加一个函数调用,而不是在调用它的同一堆栈上,所以它不会在最新的堆栈上添加堆栈。 (例如,我不知道 Erlang)
  • @Haketo 所以实际上是因为使用的函数的属性?如果它是任何其他的,例如建议的'setIntervall()',答案会不同吗?正如我所说,我是一个 JS 涉足者,试图磨练我的技能。

标签: javascript recursion tail-recursion


【解决方案1】:

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

(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. 现在,main3000 毫秒后被 JavaScript 事件循环调用。
  6. main 被调用时,loopsetTimeout 都已完成执行。
  7. main 函数有条件地调用 loop 并减少值 i
  8. main 调用loop 时,是尾调用。
  9. 但是,无论递归100 次还是10000 次都没有关系,堆栈大小永远不会增加太多而导致溢出。原因是当你使用setTimeout 时,loop 函数会立即返回。因此,当 main 被调用时,loop 已不在堆栈中。

视觉解释:

|---------------+ 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)3000 毫秒后返回main 再次调用。
  3. main 函数调用loop(1)3000 毫秒后返回main 再次调用。

因此,堆栈大小永远不会因为setTimeout 而无限增长。

阅读以下问答了解更多详情:

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

希望对您有所帮助。

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

【讨论】:

  • 绘画技巧不错。
【解决方案2】:

该代码本身不是递归的,恰恰相反,它使用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 基本上采用当前的延续并将其“抛出”到主线程。一旦主线程准备好,它就会“捕获”延续并在恢复的上下文中运行代码。

【讨论】:

  • 您能否提供有关其工作原理的更多信息?其他人谈论事件循环,所以我收集了一些关于它在做什么的信息。另一方面,我真的不明白这里发生了什么。
【解决方案3】:

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

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

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

【讨论】:

  • "独立线程",我会说,"在另一个执行堆栈上"。
  • @Haketo 是的,这就是我把它放在引号中的原因。但我会编辑它,谢谢
  • 我不知道这个东西的真名但是Javascript不是多线程的,你的函数只有在你最后一次执行函数后才会被触发。仅当当前堆栈完成时才会调用 setTimeout(..,0)。
【解决方案4】:

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

它是not supported in Node(除了版本 >6.4 和

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

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

【讨论】:

    猜你喜欢
    • 2014-06-23
    • 1970-01-01
    • 2016-03-21
    • 2018-03-17
    • 1970-01-01
    • 2017-11-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多