关于 JSLint,它不喜欢 for 循环,以及尾调用优化

Posted

技术标签:

【中文标题】关于 JSLint,它不喜欢 for 循环,以及尾调用优化【英文标题】:About JSLint, its dislike of for loops, and tail call optimization 【发布时间】:2015-08-02 20:12:59 【问题描述】:

我注意到新版本的 JSLint 不喜欢某些形式的 for 循环。我发现这很奇怪,并开始挖掘一些解释。在 JsLint 的help page 下可以找到:

ES6 最重要的新特性是正确的尾调用。这没有新的语法,所以 jsLint 看不到它。但它使递归更具吸引力,这使得循环,尤其是 for 循环,吸引力大大降低。

还有这个:

jsLint 不推荐使用 for 语句。请改用 forEach 之类的数组方法。 for 选项将抑制一些警告。 jsLint 接受的形式受到限制,不包括新的 ES6 形式。

这两个陈述都让我感到困惑。我一直认为 for 循环是最好的循环方式,主要是因为:

    一些迭代方法还没有得到很好的支持 与其他迭代方法相比,for 循环的“偷听”较少

我还在某处读到过,forEach 之类的方法不能像 for 循环一样进行优化,尽管我不确定我是否应该相信这一点。 (在 ECMAScript 5 中可能是真的?)

因此,支持其他迭代方法的一个论点是它们更具可读性,但是,随着尾调用优化开始发挥作用,它们能否与 for 循环一样快或可能更快?

我在这里假设,例如,forEach 可以进行尾调用优化,这是正确的吗?

所以,主要问题是:在 ECMAScript 6 中决定支持除 for 循环之外的迭代方法时,尾调用优化究竟是如何发挥作用的?

也许还有这样的:假设 JSLInt 更喜欢其他迭代方法是正确的,因为它们更具可读性,并且因为通过 ECMAScript 6 中的优化,它们可以和 for 一样快循环?我是否正确解释了 JSLint 帮助页面上的声明?

我知道问题应该只有一个主要问题,而我有很多;但是,我认为它们都彼此非常相关,因此我认为回答其中一个可能会回答所有问题。

谢谢,很抱歉这篇冗长的帖子!

【问题讨论】:

JSLint 比 for 循环更喜欢递归,因为 Douglas Crockford 比 for 循环更喜欢递归。差不多就是这样。他的风格非常实用。如果他不经常把这些东西强加给其他人,就好像这是事实而不仅仅是意见一样,那会很好。 他热衷于函数式编程,它使用递归进行循环。他不像其他大多数人使用 javascript 那样使用 JavaScript。 (他真的,真的应该写他自己的语言并翻译它。) 适当的尾调用优化对于函数式编程来说是至关重要的,因为没有它,它的效率真​​的很低(所有那些嵌套的函数调用,将东西压入堆栈,弹出它们,等等等等)。 所以简单来说,他是函数式的风格,但是在 ES6 之前的 JavaScript 没有尾调用优化,所以在这之前他不能真的强迫人们不喜欢 for 循环,因为效率低下没有优化的递归。现在他可以了,他也可以。如果这就是它的全部内容,那么我应该关闭这个问题。谢谢。 是的,它的大小差不多。 :-) 请注意,实际上还没有主要的 JavaScript 引擎 TCO。但他们很快就会…… 【参考方案1】:

我还在某处读到过,像 forEach 这样的方法不能像 for 循环一样被优化,尽管我不确定我是否应该相信这一点。 (在 ECMAScript 5 中可能是真的?)

forEach 很难优化,因为你有两个函数调用的开销,回调传递了三个参数(无论你是否使用它们),另外,如果你使用 mapfilter(但是不是forEach) 你也有创建新数组的开销,而不是就地做事。

因此,支持其他迭代方法的一个论点是它们更具可读性,但是,随着尾调用优化开始发挥作用,它们能否与 for 循环一样快或可能更快?

filtermap 等的主要论点是它们创建了新的数组,因此鼓励了不变性。另外,它们可以被链接起来,看起来不错且可读。如果性能确实成为问题,您可以使用while 循环或for 循环,但不要过早优化。

正如我将在下面解释的,尾调用优化不适用于迭代方法(forEachmap 等)。

我在这里假设,例如,forEach 可以进行尾调用优化,这是正确的吗?

forEach 没有优化尾调用(据我所知,取决于浏览器如何实现它)。尾调用优化只有在函数返回调用另一个函数的结果(即尾调用)时才会启动,例如:

function factorial(n)
   if(n < 2)
     return 1;
    else 
     return n * factorial(n - 1);
   
 

如果你在不支持尾调用的浏览器中运行上面的代码,如果你给它一个很大的数字(例如 10000),你会得到一个错误(RangeError: Maximum call stack size exceeded for me in Chrome),因为太多了函数将被立即调用。尾调用递归意味着 JavaScript 引擎看到您正在递归调用相同的函数,并会相应地进行优化并防止自身超过最大调用堆栈大小。

除非您在forEach 中的回调使用递归,否则尾调用优化将无济于事forEach。因为 JavaScript 之前缺少尾调用优化,所以之前的 factorial 函数将改为使用 while 循环编写。尾调用现在更适合这些递归函数而不是循环。

所以,主要问题是:在 ECMAScript 6 中,当决定支持除 for 循环之外的迭代方法时,尾调用优化究竟是如何发挥作用的?

正如我上面所说,很多人一直在使用循环而不是递归函数,这样他们就不会遇到调用堆栈大小错误。现在,通过 ES6 中的尾调用优化,这些递归函数的使用变得更加安全,并且不应该引发调用堆栈大小错误。在这些情况下,现在更喜欢使用递归而不是循环。

也许还有这样的:假设 JSLInt 更喜欢其他迭代方法是正确的,因为它们更具可读性,并且因为通过 ECMAScript 6 中的优化,它们可以和 for 循环一样快?我是否正确解释了 JSLint 帮助页面上的语句结束?

可读性是函数使用递归而不是循环(使它们更容易遵循)和尽可能使用迭代方法的一个重要原因(map 逐个值转换数组值,而不是for环形)。

如果性能成为问题,那么您可能想要使用for 循环或while 循环,但是有很多 JSLint 不喜欢的东西可能会更快。

【讨论】:

请注意,forEach 不会创建新数组,也不能链接 - 它的唯一目的是在链的末端产生副作用。在我看来,for of 几乎失去了它的用例。 @Bergi 是的,我将编辑我的答案以明确 forEach 不会创建新数组 - 但 mapfilter 会。我完全同意 - 除非你有 mapfilter 的链,否则 for offorEach 更具可读性。

以上是关于关于 JSLint,它不喜欢 for 循环,以及尾调用优化的主要内容,如果未能解决你的问题,请参考以下文章

JSlint - 在 for 循环中生成函数与评估函数

尝试重写for循环,得到jslint错误

带有闭包的 JavaScript For 循环导致 JSLint 警告

在 JavaScript (JSLint) 中的 for 循环之外引用“this”元素

jslint - 不要在循环中创建函数

需要一个标识符,而是看到 ')' 循环 jslint