Node.js尾调用优化:可能与否?

Koz*_*oss 23 javascript tail-call-optimization node.js

我喜欢的JavaScript到目前为止,并决定使用Node.js的为我的发动机的部分原因是因为这个,它声称的Node.js提供TCO.但是,当我尝试使用Node.js运行此代码(显然是尾部调用)时,会导致堆栈溢出:

function foo(x) {
    if (x == 1) {
        return 1;
    }
    else {
        return foo(x-1);
    }
}

foo(100000);
Run Code Online (Sandbox Code Playgroud)

现在,我做了一些挖掘,我找到了这个.在这里,似乎我应该这样写:

function* foo(x) {
    if (x == 1) {
        return 1;
    }
    else {
        yield foo(x-1);
    }
}

foo(100000);
Run Code Online (Sandbox Code Playgroud)

但是,这给了我语法错误.我试过它的各种排列,但在所有的情况下,Node.js的似乎不满的东西.

基本上,我想知道以下内容:

  1. Node.js是否执行TCO?
  2. 这个神奇的yield东西在Node.js中如何运作?

T.J*_*der 40

这里有两个截然不同的问题:

  • Node.js是否执行TCO?
  • 这个神奇的产量是如何在Node.js中起作用的?

Node.js是否执行TCO?

TL; DR:现在不一样了,因为节点8.x中的.它有一段时间,在一面或另一面旗帜后面,但截至本文(2017年11月),它已不复存在,因为它使用的基础V8 JavaScript引擎不再支持TCO.有关详细信息,请参阅此答案.

细节:

尾部呼叫优化(TCO)是ES2015("ES6")规范的必要部分.所以支持它不是直接的NodeJS,它是NodeJS使用的V8 JavaScript引擎需要支持的东西.

从Node 8.x开始,V8不支持TCO,甚至不支持标志.它可能(再次)在未来的某个时刻; 有关更多信息,请参阅此答案.

至少节点7.10降至6.5.0(我的注释说6.2,但node.green不同意)仅在严格模式下支持TCO后面的标志(--harmony在6.6.0及更高--harmony_tailcalls版本中).

如果你想检查你的安装,这里是node.green使用的测试(如果你使用的是相关版本,请务必使用该标志):

function direct() {
    "use strict";
    return (function f(n){
      if (n <= 0) {
        return  "foo";
      }
      return f(n - 1);
    }(1e6)) === "foo";
}

function mutual() {
    "use strict";
    function f(n){
      if (n <= 0) {
        return  "foo";
      }
      return g(n - 1);
    }
    function g(n){
      if (n <= 0) {
        return  "bar";
      }
      return f(n - 1);
    }
    return f(1e6) === "foo" && f(1e6+1) === "bar";
}

console.log(direct());
console.log(mutual());
Run Code Online (Sandbox Code Playgroud)
$ # Only certain versions of Node, notably not 8.x or (currently) 9.x; see above
$ node --harmony tco.js
true
true

这个神奇的yield东西在Node.js中如何运作?

这是ES2015的另一个东西("生成器功能"),所以V8必须实现这一点.它完全在节点6.6.0中的V8版本中实现(并且已经有多个版本)并且不在任何标志之后.

生成器函数(使用function*和使用的函数yield)通过能够停止并返回捕获其状态的迭代器来工作,并可用于在随后的场合继续其状态.Alex Rauschmeyer在这里有一篇关于他们的深入文章.

这是一个显式使用生成器函数返回的迭代器的示例,但是您通常不会这样做,我们马上就会看到原因:

"use strict";
function* counter(from, to) {
    let n = from;
    do {
        yield n;
    }
    while (++n < to);
}

let it = counter(0, 5);
for (let state = it.next(); !state.done; state = it.next()) {
    console.log(state.value);
}
Run Code Online (Sandbox Code Playgroud)

这有输出:

0
1
2
3
4

这是如何工作的:

  • 当我们调用counter(let it = counter(0, 5);)时,counter初始化调用的初始内部状态,我们立即返回迭代器; 没有counter运行中的实际代码(还).
  • 调用在第一个语句中it.next()运行代码.此时,暂停并存储其内部状态.返回带有标志和a 的状态对象.如果该标志是,则该语句产生的值.counteryieldcounterit.next()donevaluedonefalsevalueyield
  • 每次调用都会it.next()将状态推进counter到下一个状态yield.
  • 当一个呼叫it.next()品牌counter完成并返回时,状态对象,我们取回已done设置truevalue设置的返回值counter.

拥有迭代器和状态对象的变量,并调用it.next()和访问donevalue属性都是(通常)妨碍我们尝试做的事情的所有样板,因此ES2015提供了新的for-of声明,将它全部收起来我们只是给了我们每个价值.这是上面写的相同代码for-of:

"use strict";
function* counter(from, to) {
    let n = from;
    do {
        yield n;
    }
    while (++n < to);
}

for (let v of counter(0, 5)) {
    console.log(v);
}
Run Code Online (Sandbox Code Playgroud)

v对应state.value于我们之前的示例,为我们for-of执行所有it.next()调用和done检查.

  • @AndyS:纯风格编辑在任何情况下都不合适。 (3认同)