28

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

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

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

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

4

4 回答 4

42

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

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

setTimeout(function() { ... }, 1000);

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

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

(不要使用最后一个 - 称为命名函数表达式 - 实现有错误,尤其是 IE。)

相反,这是一个函数声明

function bar() { ... }

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

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

  1. 函数表达式在程序流中遇到的地方进行求值。当控制进入包含范围(例如,包含函数或全局范围)时,会评估声明。

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

您的匿名函数是函数表达式,因此除非解释器进行优化(这是免费的),否则它们将在每个循环中重新创建。因此,如果您认为实现会优化,那么您的使用就很好,但是将其分解为命名函数还有其他好处,而且——重要的是——不会花费你任何费用。此外,请参阅casablanca 的回答,了解为什么解释器可能无法优化在每次迭代中重新创建函数,具体取决于它检查代码的深度。

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

function foo() {
    for (i = 0; i < limit; ++i) {
        function bar() { ... } // <== Don't do this
        bar();
    }
}

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

为了我的钱,最好的选择是使用单个函数声明,如下所示:

function foo() {
    for (i = 0; i < limit; ++i) {
        bar();
    }

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

您会得到相同的结果,实现不可能在每个循环上创建一个新函数,您会从具有 name 的函数中受益,并且不会丢失任何东西。

于 2010-10-13T19:06:46.497 回答
24

Javascript 解释器真的会在每次迭代时创建一个函数实例吗?

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

card = $('<div>').bind('isPopulated', function (ev) { ... })

如您所知,bind可以修改对象,例如:

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

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

于 2010-10-13T19:25:03.523 回答
2

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

这就是为什么JSLint要吓唬你不要在一个紧密的循环中创建许多匿名函数。

于 2010-10-13T19:03:08.707 回答
2

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

例如:

for (var i = 0; i < 10; i++) {
  setTimeout(function () {
    alert(i)
  }, 10)
}

将导致“奇怪”的行为。这不是“在循环中创建函数,而是不理解 JS 用于变量范围和闭包的规则(变量未绑定在闭包中,范围 - 执行上下文 - 是)的问题。

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

for (var i = 0; i < 10; i++) {
  setTimeout((function (_i) { 
    return function () {
      alert(_i)
    }
  })(i), 10)
}

不好了!我仍然创建了一个函数!

于 2010-10-13T19:08:24.070 回答