【问题标题】:What are the benefits to using anonymous functions instead of named functions for callbacks and parameters in JavaScript event code?在 JavaScript 事件代码中对回调和参数使用匿名函数而不是命名函数有什么好处?
【发布时间】:2012-05-03 15:07:31
【问题描述】:

我是 JavaScript 新手。我了解该语言的许多概念,我一直在阅读原型继承模型,并且我正在用越来越多的交互式前端内容来激起我的兴趣。这是一门有趣的语言,但我总是被许多非平凡交互模型中典型的回调意大利面条所吸引。

我一直觉得奇怪的是,尽管 JavaScript 嵌套回调嵌套的可读性噩梦,但我在许多示例和教程中很少看到的一件事是使用预定义的命名函数作为回调论据。我白天是一名 Java 程序员,discarding the stereotypical jabs about Enterprise-y names for units of code 使用一种具有强大功能的 IDE 选择的语言,我最喜欢的一件事是使用有意义的(如果长的话)名称可以表达意图和意义代码更清晰,而不会使实际生产变得更加困难。那么为什么在编写 JavaScript 代码时不使用相同的方法呢?

考虑一下,我可以提出支持和反对这个想法的论据,但我对这门语言的天真和新奇使我无法得出关于为什么这在技术层面上会很好的任何结论。

优点:

  • 灵活性。带有回调参数的异步函数可以通过许多不同的代码路径之一访问,并且可能不得不编写一个命名函数来解决每个可能的边缘情况。
  • 速度。它在很大程度上影响了黑客的心态。用螺栓固定它,直到它起作用。
  • 其他人都在这样做
  • 较小的文件大小,即使是微不足道的,但在网络上的每一位都很重要。
  • 更简单的 AST?我会假设匿名函数是在运行时生成的,因此 JIT 不会将名称映射到指令,但我只是在猜测这一点。
  • 更快的调度?这个也不确定。再次猜测。

缺点:

  • 丑陋且难以阅读
  • 当您将坚果嵌套在大量回调中时,这会增加混乱(公平地说,这可能意味着您一开始就编写了结构不佳的代码,但这很常见)。
  • 对于没有功能背景的人来说,理解它可能是一个奇怪的概念

有这么多现代浏览器显示出比以前更快地执行 JavaScript 代码的能力,我看不出使用匿名回调如何获得任何微不足道的性能提升是必要的。看来,如果您处于使用命名函数可行的情况(可预测的行为和执行路径),那么没有理由不这样做。

那么,是否有任何我不知道的技术原因或陷阱导致这种做法如此普遍?

【问题讨论】:

  • 我不同意你的大多数“专业人士”,听起来你只是在猜测。也不喜欢你的缺点。底线是,对于短函数,它更有意义。就像在 Java 中一样,我们不会创建一个简短的、包含目的的命名类,我们使用匿名内部类。当复杂性需要传递可重用和/或非平凡的函数时,JS 使用任何模块模式(如果有的话)。这是到处使用的;没有看到它并不意味着在有意义的时候没有使用它。
  • @DaveNewton 我知道它就在那里,我只是想问为什么它似乎不是标准,以及为什么会出现这种情况是否存在任何客观的技术限制。至于 Java 中的匿名内部类,我只倾向于在不需要对接口进行具体实现时使用它们,比如如果我需要生成一个非常简单的 Runnable。当我这样做时,我不喜欢它。
  • 在 IE(至少某些版本)中,使用 命名函数表达式 会导致两个不同的函数实例(注意 var a = function(){...} 不是不是命名函数(表达式)。分配给a 的函数仍然是匿名的)。这就是为什么最好避免它们。当然,你总是可以使用函数声明来代替,但是如果只使用一次,似乎没有必要在作用域中创建符号。
  • @FelixKling 这似乎是一个很好的、客观的、技术性的理由来避免它们。但是函数语句/声明呢?
  • “把东西固定在它上面,直到它工作”不是黑客的心态,那是一种“我不明白我的代码是如何工作的,这对我来说都是魔法”的心态。

标签: javascript coding-style callback anonymous-function code-readability


【解决方案1】:

我使用匿名函数有三个原因:

  1. 如果不需要名称,因为该函数只在一个地方被调用,那么为什么要在您所在的任何命名空间中添加名称。
  2. 匿名函数被声明为内联,内联函数的优势在于它们可以访问父作用域中的变量。是的,您可以在匿名函数上命名,但如果它被声明为内联,那通常是没有意义的。所以内联有一个显着的优势,如果你在做内联,几乎没有理由给它命名。
  3. 当处理程序被定义在调用它们的代码中时,代码看起来更加独立和可读。您几乎可以按顺序阅读代码,而不必去查找具有该名称的函数。

我确实尽量避免匿名函数的深度嵌套,因为这可能很难理解和阅读。通常,当这种情况发生时,有更好的方法来构建代码(有时使用循环,有时使用数据表等),而命名函数通常也不是解决方案。

我想我会补充一点,如果回调开始超过大约 15-20 行,并且它不需要直接访问父范围中的变量,我会很想给它一个名字并中断将其放入在其他地方声明的自己的命名函数中。这里肯定有一个可读性点,如果将一个变长的非平凡函数放在自己的命名单元中,它就更易于维护。但是,我最终得到的大多数回调都没有那么长,而且我发现将它们保持内联更具可读性。

【讨论】:

  • 1.这个论点似乎可以扩展到包含我们命名的所有内容。 2. 这个论点似乎有点循环,但我想我明白你在说什么。 3. 目标是为函数提供非常清晰、有意义的名称,这样您就不必去找它们了。
  • @DougStephen - 在第 1 点,我只是说当回调被声明为内联时,给它一个名称并不能真正为您提供任何额外的信息。您可以从看到和声明它的上下文中确切地知道它的用途。也许这是一个学习的解释,你在看了很多构造之后就习惯了,但是现在当我看到它时,我觉得它与 for 循环没有什么不同,我认为 for 循环不需要被分解并命名为可读的名称。
  • 我明白你在说什么。这可能符合命名函数仅适用于某些上下文(可预测的行为)的想法。在这种情况下,您将不会创建内联函数,因此您可以命名它。显然将函数命名为 inline 是愚蠢的。
  • @DougStephen - 另外,不要低估第 2 点的优势。通过访问父作用域变量而不是将所有内容放入对象并传递它们,可以将某些类型的代码编写得更简单作为论据。这很方便的最常见的地方是当回调想要从原始函数访问this 指针时。您可以将其存储在父函数的局部变量中(通常以var self = this; 完成,然后您可以在匿名函数中访问self。非常非常方便。
  • 我知道它在哪里很方便,我不会反对你。但它似乎也有点副作用。将这样的信息作为参数发送下来可能是一个“麻烦”,但对我来说它似乎也更受控制。即使使用像 C 这样的语言,我也不喜欢访问外部作用域变量。
【解决方案2】:

我自己更喜欢命名函数,但对我来说归结为一个问题:

我会在其他地方使用这个功能吗?

如果答案是肯定的,我会命名/定义它。如果没有,请将其作为匿名函数传递。

如果你只使用一次,用它来挤满全局命名空间是没有意义的。在当今复杂的前端中,可能是匿名的命名函数的数量迅速增长(在真正复杂的设计上很容易超过 1000 个),通过首选匿名函数导致(相对)较大的性能提升。

不过,代码的可维护性也极其重要。每种情况都不同。如果您一开始就没有编写很多这些函数,那么无论哪种方式都没有坏处。这完全取决于您的喜好。

关于名称的另一个说明。养成定义长名称的习惯确实会损害您的文件大小。举个例子。

假设这两个函数做同样的事情:

function addTimes(time1, time2)
{
    // return time1 + time2;
}

function addTwoTimesIn24HourFormat(time1, time2)
{
    // return time1 + time2;
}

第二个确切地告诉您它在名称中的作用。第一个比较模棱两可。但是,名称中有17个字符的差异。假设该函数在整个代码中被调用了 8 次,那是您的代码不需要的 153 个额外字节。不是很大,但如果这是一种习惯,将其外推到 10 个甚至 100 个函数将很容易意味着下载量有几 KB 的差异。

然而,需要再次权衡可维护性和性能优势。这是处理脚本语言的痛苦。

【讨论】:

  • 如果您正在最小化您的 javascript 以进行部署,那么关于名称大小的点不再相关。在这种情况下选择可读性。
  • 仅当您的函数/变量在闭包内定义时(它们应该是)。任何看起来对压缩器来说可能是全局的东西都将被忽略,名称的长度仍然很重要。
  • 我会补充一点。使用匿名函数的人不会考虑产品的未来开发或支持。一个函数是要被重用的,你今天写的东西,仅仅因为你只使用一次,并不意味着其他人以后就不需要它了。我发现匿名函数是为懒惰的开发者准备的,我拒绝使用它们。
【解决方案3】:

聚会有点晚了,但一些尚未提及的功能方面,匿名或其他......

在团队之间关于代码的类人对话中不容易提及匿名函数。例如,“Joe,您能解释一下该算法在该函数中的作用。...哪一个?fooApp 函数中的第 17 个匿名函数。...不,不是那个!第 17 个!”

Anon func 对调试器也是匿名的。 (呃!)因此,调试器堆栈跟踪通常只会显示一个问号或类似的东西,当您设置多个断点时它的用处不大。您遇到了断点,但发现自己向上/向下滚动调试窗口以找出您在程序中到底在哪里,因为嘿,问号函数就是不这样做!

对污染全局命名空间的担忧是有道理的,但可以通过将函数命名为自己根对象中的节点来轻松解决,例如“myFooApp.happyFunc = function ( ... ) { ... }; ”。

在全局命名空间中可用的函数,或者像上面一样作为根对象中的节点,可以在开发和调试期间直接从调试器调用。例如,在控制台命令行中,执行“myFooApp.happyFunc(42)”。这是一种极其强大的能力,在编译的编程语言中(本机)不存在。尝试使用匿名函数。

通过将 Anon func 分配给 var,然后将 var 作为回调(而不是内联)传递,可以使它们更具可读性。例如。: var funky = 函数 ( ... ) { ... }; jQuery('#otis').click(funky);

使用上述方法,您可以将多个匿名函数分组在父函数的顶部,然后在其下方,顺序语句的内容变得更加紧密,并且更易于阅读。

【讨论】:

    【解决方案4】:

    使用命名函数更具可读性,并且它们还能够自引用,如下例所示。

    (function recursion(iteration){
        if (iteration > 0) {
          console.log(iteration);
          recursion(--iteration);
        } else {
          console.log('done');
        }
    })(20);
    
    console.log('recursion defined? ' + (typeof recursion === 'function'));
    

    http://jsfiddle.net/Yq2WD/

    当您希望有一个立即调用的函数引用自身但不添加到全局命名空间时,这很好。它仍然可读但不污染。吃你的蛋糕,然后吃。

    嗨,我叫 Jason 或者嗨,我叫 ????你选。

    【讨论】:

      【解决方案5】:

      匿名函数很有用,因为它们可以帮助您控制公开哪些函数。

      更多详细信息:如果没有名称,您不能在任何地方重新分配或篡改它,只能在它创建的确切位置。一个好的经验法则是,如果您不需要在任何地方重复使用此函数,最好考虑使用匿名函数是否能更好地防止在任何地方被篡改。

      示例: 如果你在一个有很多人的大项目上工作,如果你在一个更大的函数内部有一个函数并且你给它命名怎么办?这意味着任何与您合作并在较大功能中编辑代码的人都可以随时对较小的功能进行处理。例如,如果您将其命名为“add”,而有人将“add”重新分配给同一范围内的数字,该怎么办?然后整个事情就崩溃了!

      PS - 我知道这是一篇非常古老的帖子,但这个问题有一个更简单的答案,我希望有人在我作为初学者自己寻找答案时这样说 - 我希望你是好的,恢复旧线程!

      【讨论】:

      • 您建议的大部分内容都可以通过letconst 作用域来完成。在 2021 年使用 IIFE 的充分理由并不多。请参阅:levelup.gitconnected.com/…
      • 嘿克里斯!也感谢您的反馈。匿名函数与 IIFE 的不同,但这篇文章非常值得分享。不过,匿名函数仍然很适合了解它们的工作原理以及它们对于初学者学习基础知识的作用。特别是如果他们还没有了解模块。
      • 感谢林赛指出这一点——你当然是正确的。同意这些都是值得学习的重要概念。为了完整起见,这里有另一篇文章讨论了这两个概念之间的异同:medium.com/@DaphneWatson/…
      【解决方案6】:

      好吧,为了我的论点,为了清楚起见,以下都是我书中的匿名函数/函数表达式:

      var x = function(){ alert('hi'); },
      
      indexOfHandyMethods = {
         hi: function(){ alert('hi'); },
         high: function(){
             buyPotatoChips();
             playBobMarley();
         }
      };
      
      someObject.someEventListenerHandlerAssigner( function(e){
          if(e.doIt === true){ doStuff(e.someId); }
      } );
      
      (function namedButAnon(){ alert('name visible internally only'); })()
      

      优点:

        1234563只在这个地方着火。
      • 代码易读性获胜:在将匿名函数分配为方法的对象文字示例中,当该对象文字的全部要点是在同一个方便引用的地方添加一些相关的功能。但是,当在构造函数中声明公共方法时,我倾向于定义内联标记函数,然后将其分配为 this.sameFuncName 的引用。它让我可以在内部使用相同的方法,而无需使用“this”。当他们互相调用时,不关心定义的顺序。

      • 对于避免不必要的全局命名空间污染很有用 - 但是,内部命名空间不应该被多个团队同时广泛填充或处理,这样我觉得这个论点有点愚蠢。

      • 我同意设置短事件处理程序时的内联回调。必须寻找一个 1-5 行的函数是很愚蠢的,特别是因为使用 JS 和函数提升,定义可能会出现在任何地方,甚至不在同一个文件中。这可能是在不破坏任何东西的情况下偶然发生的,不,你并不总是能控制这些东西。事件总是导致回调函数被触发。没有理由向需要扫描的名称链添加更多链接,只是为了在大型代码库中对简单的事件处理程序进行逆向工程,并且可以通过将事件触发器本身抽象为在调试时记录有用信息的方法来解决堆栈跟踪问题模式打开并触发触发器。我实际上已经开始用这种方式构建整个界面了。

      • 当您希望函数定义的顺序很重要时很有用。有时你想确定一个默认函数是你认为的,直到代码中的某个点可以重新定义它。或者,当依赖关系被打乱时,您希望破坏更加明显。

      缺点:

      • 匿名函数不能利用函数提升。这是一个主要区别。我倾向于充分利用提升来在底部定义我自己显式命名的函数和对象构造函数,并在顶部找到对象定义和主循环类型的东西。我发现当你很好地命名你的变量时,它使代码更容易阅读,并在 ctrl-Fing 之前对正在发生的事情有一个广泛的了解,只有当它们对你很重要时才能获得详细信息。在高度事件驱动的界面中,提升也可以是一个巨大的好处,在这些界面中,对可用的内容施加严格的顺序可能会咬到你。提升有其自身的注意事项(如循环引用潜力),但如果使用得当,它是一种非常有用的工具,可用于组织和使代码清晰易读。

      • 易读性/调试。当然,它们有时会被过度使用,这会使调试和代码易读性变得很麻烦。例如,如果您不以合理的方式封装 $soup 的近乎不可避免的非繁重和大量重载的参数,那么严重依赖 JQ 的代码库可能是一个严重的 PITA 来阅读和调试。例如,JQuery 的 hover 方法是当您将两个匿名函数放入其中时过度使用匿名函数的经典示例,因为初学者很容易认为它是标准事件侦听器分配方法,而不是一个重载分配的方法一两个事件的处理程序。 $(this).hover(onMouseOver, onMouseOut) 比两个匿名函数清晰得多。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-04-10
        • 1970-01-01
        • 1970-01-01
        • 2017-04-04
        • 1970-01-01
        • 1970-01-01
        • 2019-09-12
        • 1970-01-01
        相关资源
        最近更新 更多