JSlint错误’不要在循环中创建函数’。 引出有关Javascript本身的问题

JSlint error 'Don't make functions within a loop.' leads to question about Javascript itself

我有一些代码在循环中调用匿名函数,类似于这个伪示例:

1
2
3
4
for (i = 0; i < numCards; i = i + 1) {
    card = $('').bind('isPopulated', function (ev) {
        var card = $(ev.currentTarget);
        ....

JSLint报告错误"不要在循环中创建函数"。 我喜欢保持我的代码JSLint干净。 我知道我可以将匿名函数移出循环并将其作为命名函数调用。 除此之外,这是我的问题:

Javascript解释器是否真的会为每次迭代创建一个函数实例? 或者只有一个函数实例"已编译"并且重复执行相同的代码? 也就是说,将函数移出循环的JSLint"建议"实际上是否会影响代码的效率?


部分取决于您使用的是函数表达式还是函数声明。它们是不同的东西,它们发生在不同的时间,它们对周围的范围产生不同的影响。让我们从区别开始吧。

函数表达式是function生产,您将结果用作右手值&nbsp;&mdash;例如,您将结果分配给变量或属性,或将其作为参数传递给函数,等等。这些都是函数表达式:

1
2
3
4
5
setTimeout(function() { ... }, 1000);

var f = function() {  ... };

var named = function bar() { ... };

(不要使用最后一个&nbsp;&mdash;这称为命名函数表达式&mdash;实现有bug,特别是IE。)

相反,这是一个函数声明:

1
function bar() { ... }

它是独立的,你没有将结果用作右手值。

它们之间的两个主要区别:

  • 评估函数表达式在程序流中遇到的位置。当控件进入包含范围(例如,包含函数或全局范围)时,将评估声明。

  • 函数的名称(如果有的话)在函数声明的包含范围中定义。它不适用于函数表达式(禁止浏览器错误)。

  • 你的匿名函数是函数表达式,因此禁止解释器进行优化(可以自由地进行),它们将在每个循环中重新创建。因此,如果您认为实施将进行优化,那么您的使用就可以了,但将其分解为命名函数还有其他好处,并且&nbsp;&mdash;重要的&NBSP;&MDASH;不花任何你的钱。另外,请参阅casablanca的答案,了解为什么解释器可能无法优化在每次迭代时重新创建函数,具体取决于它检查代码的程度。

    更大的问题是如果你在循环中使用函数声明,条件的主体等:

    1
    2
    3
    4
    5
    6
    function foo() {
        for (i = 0; i < limit; ++i) {
            function bar() { ... } // <== Don't do this
            bar();
        }
    }

    从技术上讲,仔细阅读规范的语法表明这样做是无效的,尽管实际上几乎没有实现强制执行。实施的内容是多种多样的,最好远离它。

    对于我的钱,最好的办法是使用单个函数声明,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function foo() {
        for (i = 0; i < limit; ++i) {
            bar();
        }

        function bar() {
            /* ...do something, possibly using 'i'... */
        }
    }

    你得到相同的结果,实现不可能在每个循环上创建一个新函数,你可以获得具有名称的函数的好处,并且你不会丢失任何东西。


    Would a Javascript interpreter really create an instance of the function per iteration?

    它必须因为它不知道函数对象是否会在别处修改。请记住,函数是标准JavaScript对象,因此它们可以具有与任何其他对象类似的属性。当你这样做:

    1
    card = $('').bind('isPopulated', function (ev) { ... })

    尽管如此,bind可以修改对象,例如:

    1
    2
    3
    function bind(str, fn) {
      fn.foo = str;
    }

    显然,如果在所有迭代中共享函数对象,这将导致错误的行为。


    嘘到JSLint。这就像头上的钝器一样。每次遇到function时都会创建一个新的函数对象(它是一个语句/表达式,而不是声明 - 编辑:这是一个白色的谎言。参见T.J.Crowders答案)。通常这是在闭包等循环中完成的。更大的问题是创建假闭包。

    例如:

    1
    2
    3
    4
    5
    for (var i = 0; i < 10; i++) {
      setTimeout(function () {
        alert(i)
      }, 10)
    }

    会导致"奇怪"的行为。这不是"在循环中创建函数而不是理解JS用于变量作用域和闭包的规则(变量没有绑定在闭包,作用域 - 执行上下文中)的问题。

    但是,您可能希望在函数中创建闭包。考虑这个不太令人惊讶的代码:

    1
    2
    3
    4
    5
    6
    7
    for (var i = 0; i < 10; i++) {
      setTimeout((function (_i) {
        return function () {
          alert(_i)
        }
      })(i), 10)
    }

    不好了!我还是创造了一个功能!


    解释器实际上可以在每次迭代时创建一个新的函数对象,只是因为该函数可能是一个需要捕获其外部作用域中任何变量的当前值的闭包。

    这就是为什么JSLint想要让你远离在紧密循环中创建许多匿名函数的原因。