【问题标题】:JavaScript: inline functions vs predefined functionsJavaScript:内联函数与预定义函数
【发布时间】:2010-03-29 15:38:20
【问题描述】:

任何机构都可以向我抛出一些关于使用 内联函数 反对将 预定义函数 名称传递给某个处理程序的论据吗?

即哪个更好:

(function() {
  setTimeout(function() { /*some code here*/ }, 5);
})();

(function() {
  function invokeMe() {
    /*code*/
  }
  setTimeout(invokeMe, 5);
})();


奇怪的问题,但我们几乎在团队中为此而争吵。

【问题讨论】:

  • 挑剔(但还没有):这两个都是内联函数,并且没有一个比另一个更预定义。唯一的区别是其中一个是未命名的,另一个是命名的。不过,这是一个显着的区别。 (好问题,顺便说一句。)
  • 我不会将“内联”一词用于未命名的函数。内联实际上意味着“用被调用函数的主体替换函数调用站点”。这很重要。
  • @neoexpert 是的,这很奇怪,我不知道为什么他的评论得到了如此多的支持。当我看到“内联”时,我想到“编译/解释代码,就好像该函数实际上并不存在,并且它的代码被复制到它被调用的任何地方”,来自 c++ 世界:cplusplus.com/articles/2LywvCM9en.wikipedia.org/wiki/Inline_function
  • 好吧,根据here,他们使用“内联”作为匿名函数(函数表达式),然后将其分配给变量。奇怪。

标签: javascript coding-style


【解决方案1】:

命名函数

此页面上的问题和答案存在严重的术语误用。函数是否是内联(函数表达式)并没有说明你不能命名它。

这是使用函数表达式

setTimeout(function doSomethingLater() { alert('In a named function.'); }, 5);

这是使用函数语句

function doSomethingLater() { alert('In a named function.'); }
setTimeout(doSomethingLater, 5);

两个示例都使用命名函数,并且在调试和分析工具方面都获得了相同的好处!

如果指定了名称(“函数”之后但括号之前的文本),那么它就是一个命名函数,无论它是内联的还是单独声明的。如果未指定名称,则为“匿名”。

注意:T.J.指出 IE 以一种不平凡的方式错误处理命名函数表达式(请参阅:http://kangax.github.com/nfe/#jscript-bugs),这很重要,我只是想说明一下术语。

您应该使用哪个?

在回答您的直接问题时,如果该函数可以在代码中的任何其他位置使用,您应该使用命名函数语句。如果函数只在一个地方使用并且在其他任何地方都没有相关性,那么我会使用函数表达式,除非它太长或者感觉不合适(出于风格原因)。如果您使用内联函数表达式,那么出于调试或代码清晰的目的,无论如何命名它通常很有用。

内存泄漏

无论是命名函数、使用函数语句还是使用函数表达式,对内存泄漏问题的影响很小。让我尝试解释导致这些泄漏的原因。看看这段代码:

(function outerFunction() {
    var A = 'some variable';

   doStuff();
})();

在上面的代码中,当“outerFunction”完成时,“A”超出范围,可以被垃圾回收,释放内存。

如果我们在里面添加一个函数呢?

(function outerFunction() {
    var A = 'some variable';

   setTimeout(function(){ alert('I have access to A whether I use it or not'); }, 5);
})();

在这段代码(上面)中,我们传递给 setTimeout 的函数表达式有一个对“A”的引用(通过闭包的魔法),即使在“outerFunction”完成之后 “A”也会保留在内存中,直到超时被触发,函数被取消引用

如果我们将该函数传递给 setTimeout 以外的其他函数会怎样?

(function outerFunction() {
    var A = 'some variable';

   doStuff(function(){ alert('I have access to A whether I use it or not'); });
})();

function doStuff(fn) {
    someElement.onclick = fn;
}

现在我们传递给“doStuff”的函数表达式可以访问“A”,即使在“outerFunction”完成后只要有对我们传递的函数的引用,“A”就会保留在内存中进入doStuff。在这种情况下,我们正在创建对该函数的引用(作为事件处理程序),因此“A”将保留在内存中,直到该事件处理程序被清除。 (例如有人打电话给someElement.onclick = null

现在看看当我们使用函数语句时会发生什么:

(function outerFunction() {
    var A = 'some variable';

    function myFunction() { alert('I have also have access to A'); };
    doStuff(myFunction);
})();

同样的问题!只有当“doStuff”不包含对它的引用时才会清理“myFunction”,并且只有在清理“myFunction”时才会清理“A”。我们使用的是语句还是表达式都没有关系;重要的是是否在“doStuff”中创建了对该函数的引用!

【讨论】:

  • 你能建议如何解决这个内存泄漏问题吗?
  • @th1rdey3,一旦您了解了 javascript 中如何管理范围,就更容易避免。关键是:保持函数小,并以尽可能低的可见性创建变量。 这意味着:不要创建全局变量,函数应该只做一件事,如果变量只需要在函数,在该函数中对其进行 var。就是这样。
  • 我从没想过闭包是内存泄漏。如果您了解它们,它们会非常有用。
【解决方案2】:

两者有一个显着的区别:后者有名字。

我喜欢帮助我的工具帮助我,所以我主要是avoid anonymous functions,因为我的工具无法为我提供关于它们的有意义的信息(例如,在调试器中的调用堆栈列表中等)。所以我会选择

(function(){
  function invokeMe() {
    /*code*/
  }
  setTimeout(invokeMe, 5);
})();

...一般形式。然而,规则是用来打破的,而不是一味地屈从。 :-)

请注意,根据规范,还有第三种选择:您可以拥有一个也有名称的内联函数:

(function(){
  setTimeout(function invokeMe(){ /*some code here*/ }, 5);
})();

不过,问题在于,到目前为止,Microsoft 的每个版本的 JavaScript 解释器(“JScript”),包括(令人惊讶的是)IE9 中的版本,都错误地处理了命名函数表达式并创建两个在不同时间完全不同的功能。 (Proof,请在 IE9 或更早版本以及几乎任何其他浏览器中尝试。)IE 以两种方式出错:1. 它创建两个单独的函数对象,以及 2. 作为其中之一的结果,它将名称符号“流血”到表达式的封闭范围中(明显违反规范的Section 13)。详情在这里:Double take

【讨论】:

  • @Mark:谢谢。你的不客观?对我来说似乎是这样。
  • @TJ 重用是,但这很明显。在什么时候大量的名称变成污染将是主观的,以及任何可读性的论点。很难与堆栈跟踪中的更多细节争论。
  • +1 表示“规则是用来打破的,而不是一味地屈服于”
  • 似乎对“命名函数”有误解。函数是否内联(函数表达式)与否(函数语句)并不影响它是否可以命名。
  • 例如,setTimeout(function myFunc() {}, 5) 使用命名函数,即使它是内联函数表达式而不是函数语句。
【解决方案3】:

IMO,仅当您打算以后以其他方式重新使用它时,声明一个函数才有用。

我个人将函数表达式(第一种方式)用于setTimeout 处理程序。

但是您可能想知道函数声明和函数表达式之间的区别,我推荐您阅读以下文章:

【讨论】:

  • 给函数命名也是避免内存泄漏的最好方法。
  • @HeavyWave:你所说的需要澄清。确实,创建闭包的函数更容易发生内存泄漏(如果您不小心的话),即使没有导致内存泄漏,它们也会使用比所需更多的内存(因为您通常不需要关闭)。然而,命名一个函数并不意味着你避免了闭包。使用全局命名函数可以。但是谁愿意用一堆全局函数编写一个严肃的 js 应用程序呢?网页增强没问题。不过,在这种情况下,内存就不是什么大问题了。
  • @Juan Mendes,你是完全正确的。我应该澄清我的意思。当然,简单地命名一个函数并不能保证不会有泄漏。我只是想指出,滥用匿名函数会导致严重的内存泄漏问题,这对 Web 应用程序来说至关重要。
  • @Egor,与命名函数语句相比,匿名函数(未命名函数表达式)绝对不会导致更多内存泄漏。这些“泄漏”是由永远不会离开作用域的闭包引起的,并且会或不会发生取决于函数的声明位置和函数的使用方式,而不是您使用的函数类型。
  • @Juan,所有函数都创建闭包!重要的是对这些函数的引用是否正确地超出范围允许这些闭包被垃圾收集,这与您是否使用表达式无关。
【解决方案4】:

我建议对立的团队成员之间进行全面决斗来解决此类争论。

更严重的是,到最后都无所谓。第一种形式(未命名的函数)对于较大的函数往往会变得笨拙,但对于小型(1-2 行)函数来说根本不是什么大问题。第二种形式同样无害。

反对任何一种风格的任何论点都是纯粹的bikeshedding,imo。

【讨论】:

  • +1 用于组合决斗、一个好的答案和自行车棚 :) 这也是我们在这里获得普遍认可的方式:将内联函数限制为几行。
  • +1 表示“完整决斗”评论。 -1 表示自行车脱落评论。实际上,即使没有重用,这两种形式之间也存在非常显着(阅读:非平凡)的差异。
  • @TJ Crowder - 当然,存在显着差异。我的假设是,这些差异与他们的辩论并不真正相关(即,在他们讨论的情况下,两种形式的功能相似,而一些开发人员只是根据风格争论(即使他们另有说法))。但你是对的“任何论点”都是夸大其词。 :)
  • 感谢您提供的bikeshedding 链接,不知何故以前从未见过。
【解决方案5】:

内联函数避免了命名空间污染,并且预定义函数具有更高的重用性。我认为您可以在每个合适的情况下提出案例。

【讨论】:

  • 在他的示例中,在这两种情况下基本上都存在 no 命名空间污染。他将整个东西包裹在一个作用域函数中,从而为命名函数创建了一个定义明确、包含的命名空间。没有创建全局。
  • @T.J.没错,但从字面上看,在调用堆栈中也没有什么可调试的,也不需要工具提供任何信息。 ;)
  • 我们不知道/*code*/ 在哪里会有什么复杂的逻辑! :-) 它可能会调用一个调用另一个调用(等等)的函数的函数,他可能需要调试它。 (或者它可能只是将文档标题设置为“Foo!”,这似乎不太需要调试。)
  • @TJ:我知道。 :) 我只是想指出所有答案中都有假设。一个是/*code*/ 中的代码是不平凡的,另一个是范围函数中包含的内容比示例中包含的内容更多。
  • 嗯,好的。 :-) 但我只想指出,在一种情况下,他确实(有效地)说“这里有代码”。但是,正如你所说,假设。
【解决方案6】:

预定义的命名函数可以减少http://callbackhell.com/提到的JavaScript回调地狱问题

【讨论】:

    【解决方案7】:

    我认为这样的代码的唯一区别是,使用第二段代码可以重新调用相同的函数(有时使用“计时器函数”很有用):

    (function(){
      function invokeMe() {
        if(..) setTimeout(invokeMe, 5);
      }
      setTimeout(invokeMe, 5);
    })();
    

    【讨论】:

    • @Pointy: True -- 在几乎所有的实现中都以严重的性能成本(我们在这里谈论数量级),而不是在新的“严格”模式下.
    • @Pointy:是的,当我了解到性能方面时,我真的很震惊。
    • @TJ 我猜你已经看过关于 IE w.r.t. 的东西了。用作右值的命名函数yura.thinkweb2.com/named-function-expressions 仍然是一篇非常好的文章
    • @Pointy:哈哈,哦,是的,我知道 kangax 是因为我们共同参与了 Prototype。实际上,我在回答中链接到的博客文章中引用了那篇文章。 ;-) 而且不仅仅是 IE,在他写那篇文章的时候,很多实现都有问题。
    【解决方案8】:

    我们不能相处融洽吗?

    (function(){
      setTimeout( (function InvokeMe(){ /*some code here*/ }), 5);
    })();
    

    只有一件事真正重要,IMO,那就是易于调试。除了它是匿名的并且具有 args 的事实之外,许多步骤跟踪器将无法告诉您有关该 func 的任何信息,但您仍然可以通过将定义放在括号中以强制评估来定义内联名称。对于非常简单或明显的破坏功能,我想这没什么大不了的,但对我来说就像半决赛一样。如果它不会引起疼痛,我真的不在乎其他人会做什么,但我总是尝试命名我的功能,因为它并不难,而且它可能是一个优势。

    【讨论】:

      【解决方案9】:

      我知道,这是一个老问题, 但对我来说,还有一个比已经提到的更重要的区别。 吊装 每个函数都必须创建,因此会在内存中保留一些空间,最终必须在以后进行 GC。

      命名函数被提升到周围函数的开头,因此在每次函数调用时都会创建,无论它们是否被使用。 只有在执行定义它们的代码时才会创建匿名函数。

      //an example where you wold prefer to use an anonymous function.
      //you can assign this (anonymous) function to a variable, so you get your "name" back.
      function someFn(){
          if(condition){
              //the variable declaration has been hoisted, 
              //but the function is created at this point, and only if necessary.
              var process = function(value){/* */};
              switch(condition2){
                  case 1: process(valueFor1); break;
                  case 2: process(valueFor2); break;
                  /* ... */
              }
          }
      }
      
      function someFn(){
          var process;
          if(condition){
              process = function(value){ /* A */ }
          }else{
              process = function(value){ /* B */ }
          }
      
          //beware, depending on your code, "process" may be undefined or not a function
          process(someValue);
      }
      
      
      //an example where you would prefer (/ utilize) the hoisting.
      function someFn(){
          /* some code */
          while(condition){
              //some might want to keep the function definition near the code where it is used,
              //but unlike an anonymous function or a lambda-expression this process-function 
              //is created only once per function-call, not once per iteration.
              function process(value, index){ /* ... */ }
              /* ... */
              process(value, index)
          }
      }
      

      所以,根据经验:

      • 在循环内不应有匿名函数或 lambda 表达式

      • 如果您只需要在(很少为真)条件内使用该函数,您应该更喜欢匿名函数而不是命名函数,因为它们仅在需要时创建

      • 如果您了解您的业务 (JavaScript),您就知道何时可以忽略此建议

      【讨论】:

        【解决方案10】:

        没有技术上的理由偏爱一个版本而不是另一个版本。对我来说通常取决于两件事:

        1. 我想在另一个上下文中重新使用传递的回调。在这种情况下,我独立定义函数并传递引用。
        2. 回调大于~10 行代码,函数需要回调后的附加参数。在这种情况下,很难重构哪些值实际上是传递给函数的。

        例子:

        setTimeout(function() { // I need to scroll to see the other arguments
        
          // many lines of code
        
        }, 0); // <- where does this '0' belong to?
        

        【讨论】:

        • 是否还有不显示匹配括号的编辑器?
        【解决方案11】:

        我更喜欢使用命名函数。命名函数在所有调试器(air、firebug、IE)上按名称显示。

        例子:

        请注意,您还可以使用内联命名函数,例如

        {
            method: function obj_method(){}
        }
        

        这样,当您查看堆栈跟踪时,您将看到函数 obj_method 而不是匿名的。

        您是否询问何时内联函数而不是声明它?当它在代码中有意义时。如果您从两个不同的地方需要它,则不能内联。有时内联使代码更容易阅读,有时更难。

        【讨论】:

        • @Juan:命名函数表达式 (method: function obj_method(){}) 在大多数 JavaScript 实现(包括 IE 的 JScript)中都不起作用。它应该工作,可以说,但它没有。有关更多信息,请参阅 Juriy Zaytsev 关于 NFE 的文章:yura.thinkweb2.com/named-function-expressions(此链接实际上在其他答案之一中,也在对 another 答案的评论中。)
        • @TJ 我阅读了有关调试器中函数名称的部分。那篇文章解释说存在一些怪癖,并没有说内联命名函数在 IE 中不起作用。大多数怪癖可以通过确保在同一范围内永远不会有两个同名的函数来解决。因此,如果您有一个块来决定返回哪个函数,只需相应地命名它们,例如 addEventIE 和 addEventStandard 就可以了。这篇文章实际上说:“归根结底,命名函数表达式是获得真正强大的堆栈检查的唯一方法。”
        【解决方案12】:

        我也倾向于命名函数。匿名函数引用很快,但应该只用于简单的东西。我的经验是,如果函数超过 2 行代码,它可能属于它自己的定义。

        大多数使用匿名函数的示例代码使这变得复杂。但是样本通常非常简单。随着事情变得更加复杂,该方法分崩离析。我已经看到函数引用嵌套在函数引用中,因为开发人员意识到在后续步骤中需要更多回调。而不是这种基于树的逻辑,我更喜欢隔离功能的组织。

        通常我很高兴我可以重用我稍后定义的函数之一。

        匿名函数的一个重要用途是当您需要将作用域数据传递给函数调用时,但我通常只是将我的函数包装到匿名函数中。

        如果您进入测试驱动开发,命名函数也是绝对必要的。

        【讨论】:

          【解决方案13】:

          在提供的示例中,函数的声明和使用非常接近,我认为唯一的区别是可读性。我更喜欢第二个例子。

          【讨论】:

            猜你喜欢
            • 2013-06-14
            • 2010-11-11
            • 2014-02-13
            • 1970-01-01
            • 1970-01-01
            • 2012-09-07
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多