关于 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
很难优化,因为你有两个函数调用的开销,回调传递了三个参数(无论你是否使用它们),另外,如果你使用 map
或 filter
(但是不是forEach
) 你也有创建新数组的开销,而不是就地做事。
因此,支持其他迭代方法的一个论点是它们更具可读性,但是,随着尾调用优化开始发挥作用,它们能否与 for 循环一样快或可能更快?
filter
和 map
等的主要论点是它们创建了新的数组,因此鼓励了不变性。另外,它们可以被链接起来,看起来不错且可读。如果性能确实成为问题,您可以使用while
循环或for
循环,但不要过早优化。
正如我将在下面解释的,尾调用优化不适用于迭代方法(forEach
、map
等)。
我在这里假设,例如,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
不会创建新数组 - 但 map
和 filter
会。我完全同意 - 除非你有 map
和 filter
的链,否则 for of
比 forEach
更具可读性。以上是关于关于 JSLint,它不喜欢 for 循环,以及尾调用优化的主要内容,如果未能解决你的问题,请参考以下文章
带有闭包的 JavaScript For 循环导致 JSLint 警告