使用匿名函数是否会影响性能?

97
我一直在想,在Javascript中使用命名函数和匿名函数是否会有性能差异?
for (var i = 0; i < 1000; ++i) {
    myObjects[i].onMyEvent = function() {
        // do something
    };
}

vs

function myEventHandler() {
    // do something
}

for (var i = 0; i < 1000; ++i) {
    myObjects[i].onMyEvent = myEventHandler;
}

第一种方法更加整洁,因为它不会在代码中堆积很少使用的函数,但是重复声明该函数多次是否有影响呢?


1
我知道这不是问题中的内容,但是关于代码清洁度/可读性,我认为“正确的方式”应该在中间某个地方。罕用的顶级函数的“杂乱无章”很烦人,但是过度嵌套的代码也很烦人,它依赖于在其调用时内联声明的匿名函数(想想node.js回调地狱)。前者和后者都可能使调试/执行跟踪变得困难。 - Zac B
3
以下性能测试会对该函数进行数千次迭代。即使您看到了显著的差异,大多数使用情况不会以那种数量级的迭代方式运行。因此,最好选择适合您需求的选项,并忽略本特定情况下的性能。 - user
@nickf 当然这是一个很老的问题,但请看最新更新的答案。 - Chandan Pasunoori
12个回答

95

这里的性能问题在于每次循环都创建一个新的函数对象的成本,而不是使用匿名函数的事实:

for (var i = 0; i < 1000; ++i) {    
    myObjects[i].onMyEvent = function() {
        // do something    
    };
}

即使这些函数具有相同的代码主体并且没有绑定到词法作用域(闭包),您也创建了1000个不同的函数对象。另一方面,以下代码似乎更快,因为它只是将相同的函数引用分配给循环中的数组元素:

function myEventHandler() {
    // do something
}

for (var i = 0; i < 1000; ++i) {
    myObjects[i].onMyEvent = myEventHandler;
}

如果您在进入循环之前创建匿名函数,然后仅在循环内部将其引用分配给数组元素,您会发现与命名函数版本相比,性能或语义上没有任何区别:

var handler = function() {
    // do something    
};
for (var i = 0; i < 1000; ++i) {    
    myObjects[i].onMyEvent = handler;
}

简而言之,使用匿名函数与使用命名函数在性能方面没有明显的成本区别。

附带一提,从上面来看,似乎以下两种方式没有差别:

function myEventHandler() { /* ... */ }

和:

var myEventHandler = function() { /* ... */ }

前者是一个函数声明,而后者是对匿名函数的变量赋值。虽然它们可能看起来具有相同的效果,但JavaScript会稍微不同地处理它们。为了理解这种差异,我建议阅读“JavaScript函数声明的歧义”。

任何方法的实际执行时间很大程度上由浏览器实现的编译器和运行时决定。要完整比较现代浏览器的性能,请访问JS Perf网站


你在函数体前忘记了括号。我刚测试过,它们是必需的。 - Chinoto Vokro
看起来基准测试结果非常依赖于 JS 引擎! - aleclofabbro
3
JS Perf示例中是否存在一个缺陷:第一种情况仅定义了函数,而第二种和第三种情况似乎意外调用了该函数。 - bluenote10
那么按照这种推理,开发node.js网络应用程序时,创建函数并将它们作为回调传递比创建匿名回调更好? - Xavier Mukodi

23

以下是我的测试代码:

var dummyVar;
function test1() {
    for (var i = 0; i < 1000000; ++i) {
        dummyVar = myFunc;
    }
}

function test2() {
    for (var i = 0; i < 1000000; ++i) {
        dummyVar = function() {
            var x = 0;
            x++;
        };
    }
}

function myFunc() {
    var x = 0;
    x++;
}

document.onclick = function() {
    var start = new Date();
    test1();
    var mid = new Date();
    test2();
    var end = new Date();
    alert ("Test 1: " + (mid - start) + "\n Test 2: " + (end - mid));
}
结果:
测试1:142毫秒 测试2:1983毫秒
看起来JS引擎没有意识到Test2中是相同的函数,每次都编译。

3
这项测试是在哪个浏览器上进行的? - andynil
5
Chrome 23 的时间:(2ms/17ms),IE9:(20ms/83ms),FF 17:(2ms/96ms)。 - Davy8
你的回答值得更多的重视。我的Intel i5 4570S上的运行时间:Chrome 41(1/9),IE11(1/25),FF36(1/14)。显然,在循环中使用匿名函数性能较差。 - ThisClark
8
这个测试并没有看起来那么有用。在这两个例子中,内部函数实际上都没有被执行。事实上,这个测试只是在展示创建一个函数一千万次比创建一个函数快的效果。 - Owen Allen
这个测试表明引擎无法识别新创建的函数始终相同,但2021年的差异较小。 - licancabur

3
作为一个通用的设计原则,你应该避免多次实现相同的代码。相反,你应该将公共代码提取到一个函数中,并从多个地方执行该函数(通用、经过充分测试、易于修改)。
如果(与你从问题中推断出的不同),你只声明了内部函数并且只在一个地方使用了该代码(并且程序中没有其他相同的内容),那么匿名函数可能(这是一个猜测)会被编译器视为普通命名函数。
这是一个非常有用的特性,但不应在许多情况下使用。

1

我们可以在声明函数的操作中产生性能影响。下面是在另一个函数内部或外部声明函数的基准测试:

http://jsperf.com/function-context-benchmark

在Chrome中,如果我们将函数声明在外部,操作速度会更快,但在Firefox中情况则相反。
在另一个例子中,我们可以看到,如果内部函数不是纯函数,在Firefox中也会出现性能问题: http://jsperf.com/function-context-benchmark-3

1

我不会期望有太大的差异,但如果有差异,可能会因脚本引擎或浏览器而异。

如果您发现代码更易于理解,性能就不是问题,除非您希望调用该函数数百万次。


0
正如评论中提到@nickf的答案:回答

创建一个函数一次比创建一百万次函数更快

是肯定的。但正如他的JS表现所示,它不会慢一百万倍,而是随着时间的推移变得更快。

对我来说更有趣的问题是:

重复的创建+运行与一次创建+重复的运行相比如何?

如果函数执行复杂计算,则创建函数对象的时间很可能可以忽略不计。但在运行快速的情况下,创建的开销怎么样?例如:

// Variant 1: create once
function adder(a, b) {
  return a + b;
}
for (var i = 0; i < 100000; ++i) {
  var x = adder(412, 123);
}

// Variant 2: repeated creation via function statement
for (var i = 0; i < 100000; ++i) {
  function adder(a, b) {
    return a + b;
  }
  var x = adder(412, 123);
}

// Variant 3: repeated creation via function expression
for (var i = 0; i < 100000; ++i) {
  var x = (function(a, b) { return a + b; })(412, 123);
}

这个JS Perf显示,只创建一次函数比预期更快。然而,即使是像简单加法这样非常快的操作,重复创建函数的开销也仅占几个百分点。

差异可能只在创建函数对象复杂且运行时间可忽略的情况下变得显著,例如,如果整个函数体被包装在if (unlikelyCondition) { ... }中。


0

以下方式能够确保你的循环在各种浏览器中更快,尤其是IE浏览器:

for (var i = 0, iLength = imgs.length; i < iLength; i++)
{
   // do something
}

你在循环条件中随意放了一个1000,但如果你想遍历数组中的所有项,你应该明白我的意思。


0

@nickf

虽然这是一个相当愚蠢的测试,因为你正在比较执行和编译时间,显然方法1(编译N次,取决于JS引擎)的成本比方法2(只编译一次)要高。我无法想象有JS开发人员会以这种方式编写代码来通过试用期。

更现实的方法是匿名分配,事实上你为document.onclick方法使用的就是以下方法,这实际上略微偏爱于匿名方法。

使用类似于您的测试框架:


function test(m)
{
    for (var i = 0; i < 1000000; ++i) 
    {
        m();
    }
}

function named() {var x = 0; x++;}

var test1 = named;

var test2 = function() {var x = 0; x++;}

document.onclick = function() {
    var start = new Date();
    test(test1);
    var mid = new Date();
    test(test2);
    var end = new Date();
    alert ("Test 1: " + (mid - start) + "ms\n Test 2: " + (end - mid) + "ms");
}

0
一个引用几乎总是比它所引用的东西慢。就像这样思考一下 - 假设你想打印添加 1 + 1 的结果。哪个更有意义:
alert(1 + 1);

或者

a = 1;
b = 1;
alert(a + b);

我知道这是一个非常简单的方式来看待它,但是它是有说明性的,对吧?只在多次使用时使用引用——例如,哪个例子更有意义:

$(a.button1).click(function(){alert('you clicked ' + this);});
$(a.button2).click(function(){alert('you clicked ' + this);});

或者

function buttonClickHandler(){alert('you clicked ' + this);}
$(a.button1).click(buttonClickHandler);
$(a.button2).click(buttonClickHandler);

第二个是更好的实践,即使它有更多的行。希望所有这些都有所帮助。(而且jquery语法没有让任何人感到困惑)

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接