JavaScript事件代码中,使用匿名函数作为回调函数和参数与使用命名函数相比有哪些好处?

77
我对JavaScript还比较新。我理解这门语言的许多概念,已经阅读了有关原型继承模型的文章,并且越来越多地涉足交互式前端内容。这是一门有趣的语言,但我总是被非常复杂的交互模型中充斥的回调“意大利面条”所吓到。
有一件事情一直让我感到奇怪,尽管JavaScript嵌套回调的可读性很差,但在许多示例和教程中,我很少看到预定义的命名函数用作回调参数。我白天是一名Java程序员,虽然抛开关于企业级名称的刻板印象,但我喜欢使用有意义(尽管很长)的名称,在具备强大功能的IDE中工作,可以使代码的意图和含义更加清晰,而不会增加实际生产效率。那么为什么在编写JavaScript代码时不采用相同的方法呢?
想了一下,我能够想出支持和反对这个想法的论点,但是由于我的天真和对这门语言的新鲜感,我无法得出任何结论,也不知道在技术层面上这样做是否好。

优点:

  • 灵活性。带有回调参数的异步函数可以通过许多不同的代码路径到达,如果为每个可能的边缘情况编写命名函数可能会很麻烦。
  • 速度。这在黑客心态中非常重要。添加组件直到它起作用。
  • 其他人都在使用它
  • 更小的文件大小,即使微不足道,但在网络上每一位都很重要。
  • 更简单的AST?我想匿名函数是在运行时生成的,所以JIT不会干扰将名称映射到指令,但这只是我的猜测。
  • 更快的分派?对此我也不确定。再次猜测。

缺点:

  • 它很丑陋且难以阅读
  • 当您嵌套在回调的深渊中时,它会增加混乱(但公正地说,这可能意味着您一开始就编写了构造不良的代码,但这很常见)。
  • 对于没有函数背景的人来说,理解它可能是一个奇怪的概念
由于许多现代浏览器能够比以前更快地执行JavaScript代码,我认为使用匿名回调函数所能获得的微小性能提升并不是必要的。如果您处于可以使用命名函数的情况下(可预测的行为和执行路径),那么没有理由不这样做。那么,使用匿名回调函数如此普遍的原因是否存在任何技术上的原因或需要注意的问题呢?

1
我不同意你的大部分“优点”,听起来像是在猜测。我也不喜欢你的缺点。归根结底,对于短函数来说,使用匿名函数更合理。就像在Java中,我们不为了短暂而受限制的目的而创建命名类,而是使用匿名内部类。当复杂性需要传递可重用和/或非平凡函数时,JS使用各种模块模式。这被广泛使用;没有看到它并不意味着在合适的时候不会被使用。 - Dave Newton
@FelixKling 这似乎是一个很好的、客观的、技术上避免它们的理由。但是函数语句/声明呢? - Doug Stephen
@FelixKling在这种情况下,IE是否有任何问题? - Doug Stephen
1
“把东西一直往上堆,直到它可以工作”并不是黑客的心态,这反映的是“我不理解我的代码是如何工作的,对我来说它都是魔法”的心态。 - mu is too short
@DougStephen 嗯,对我来说,听起来你总体上对匿名函数/类有一些抵触,但并没有提供一个技术原因。它们只是特定工作的正确工具,如果不使用它们进行某些类型的Java编程将是...荒谬的。 - Dave Newton
显示剩余5条评论
6个回答

66

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

  1. 如果函数仅在一个地方调用,不需要名称,则为什么要将名称添加到所在的命名空间中。
  2. 匿名函数是内联声明的,内联函数具有优势,因为它们可以访问父作用域中的变量。是的,您可以给匿名函数命名,但如果它是内联声明的,那通常没有意义。因此,内联具有显着的优势,如果您正在进行内联,就很少有理由给它命名。
  3. 当处理程序在调用它们的代码右侧定义时,代码似乎更具自包含性和可读性。您可以按几乎连续的方式阅读代码,而不必去查找带有该名称的函数。

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

我想我应该补充一句,如果回调开始变得超过15-20行,并且不需要直接访问父作用域中的变量,则我会考虑给它一个名称并将其分解为在其他地方声明的命名函数。在此处确实存在可读性问题,如果将非平凡的长函数放入自己的命名单元中,则更易维护。但是,我结束时得到的大多数回调都不那么长,并且我发现将它们保持内联更易读。


3
  1. 这个论点似乎可以扩展到我们给任何事物命名的范围。
  2. 这个论点似乎有点循环,但我认为我理解你的意思了。
  3. 目标是给函数起非常清晰、有意义的名称,这样你就不需要去查找它们。
- Doug Stephen
1
@DougStephen - 在第一点上,我只是想说当回调函数内联声明时,给它命名并不能为你提供任何额外的信息。你可以从它所在的上下文和声明中准确地知道它的用途。也许这是一个经过学习的解释,在看到这个结构很多次后你会习惯于它,但现在当我看到它时,它对我来说与for循环没有什么区别,我认为for循环不需要被拆分并赋予名称才能阅读。 - jfriend00
我明白你的意思。这可能符合在某些情况下(可预测的行为)仅在特定上下文中命名函数的想法。在这种情况下,您不会创建内联函数,因此可以对其进行命名。显然,在内联命名函数是愚蠢的。 - Doug Stephen
@DougStephen - 同时,不要低估第二点的优势。有些类型的代码可以通过访问父级作用域变量而不是将所有内容放入对象并将它们作为参数传递来简化编写。最常见的情况是当回调函数想要访问原始函数中的“this”指针时。您可以将其存储在父函数的本地变量中(通常使用var self = this;),然后您可以在匿名函数中访问self。非常方便。 - jfriend00
顺便提一下,为匿名函数命名有其自身的优势,最大的一个优势是当你在堆栈跟踪上看到某个东西失败时,你会看到函数的名称而不是“anonymous”。这也意味着读者不必回头查API以弄清哪个参数是成功哪个是失败,它可以自我说明! - TechMaze
显示剩余9条评论

12

个人更喜欢自己命名的函数,但问题归结于一个问题:

我是否会在其他地方使用这个函数?

如果答案是肯定的,我就给它命名/定义。如果不是,我就将其作为匿名函数传递。

如果你只使用一次,那么通过将其作为匿名函数而不是命名函数传递,就不会在全局名称空间中产生冗余。在今天复杂的前端中,本来可以是匿名函数的命名函数数量很快就会增长(在真正复杂的设计中可能超过1000个),通过优先使用匿名函数可以获得较大的性能提升。

然而,代码可维护性也非常重要。每种情况都不同。如果你本来就不会写很多这样的函数,无论怎样做都没有关系。这完全取决于你的个人喜好。

关于命名的另一个注意事项。习惯定义长名称会使文件大小变大。看下面这个例子。

假设这两个函数都执行相同的操作:

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

function addTwoTimesIn24HourFormat(time1, time2)
{
    // return time1 + time2;
}
第二个函数的名称非常明确地告诉你它做了什么。而第一个函数则含义不太明确。然而,这两个函数的名称有17个字符的差异。如果在代码中被调用了8次,那么就会额外增加153个字节的代码量。虽然不是很大的数据量,但如果习惯于这样写,在10个甚至100个函数中推广使用将会导致下载量增加几KB。
然而,需要权衡可维护性与性能的好处。这是处理脚本语言时的痛苦之处。

12
如果你在部署时要最小化 JavaScript 文件,那么有关名称大小的问题就不再重要。在这种情况下,应优先考虑代码的可读性。 - Paddy
2
只有在闭包内定义函数/变量(它们应该是这样的)时,才会被压缩。任何看起来可能是全局的内容都将被保留,名称的长度仍然很重要。 - Andrew Ensley
我想补充一点..使用匿名函数的人并没有考虑产品未来的开发或支持。函数应该是可重用的,你今天写的东西,仅因为你只使用它一次,并不意味着其他人以后不需要它。我认为匿名函数是懒惰的开发者使用的,我拒绝使用它们。 - user1853517

11
有一些关于函数的方面还没有被提到,特别是匿名函数……
在团队讨论代码时,匿名函数不容易被引用。例如,“Joe,请你解释一下这个算法在哪个函数中实现…… 是哪一个?fooApp函数中的第17个匿名函数…… 不是那一个! 第17个!”
匿名函数对调试器也是匿名的。(当然!) 因此,调试器堆栈跟踪通常只会显示一个问号或类似的东西,使得在设置多个断点时它变得不太有用。您触发了断点,但是发现自己在滚动调试窗口上下查看自己程序中的位置,因为嘿,问号函数就是不行!
担心污染全局命名空间的问题是合理的,但可以通过将函数命名为您自己的根对象中的节点来轻松解决,例如“myFooApp.happyFunc = function(...) {...};”。
在全局命名空间中或者像上面那样作为根对象的节点可直接从调试器中开发和调试时调用。例如,在控制台命令行中执行“myFooApp.happyFunc(42)”。这是一种在编译型编程语言中不存在的极其强大的能力。试着在匿名函数中试试。
通过将匿名函数分配给变量,然后将变量作为回调传递(而不是内联),可以使匿名函数更易读。例如:
var funky = function(...) {...}; jQuery('#otis').click(funky);
使用上述方法,您可以在父函数的顶部可能组合几个匿名函数,然后下面的连续语句的实质就变得更加紧密组合,并且更容易阅读。

4

匿名函数非常有用,因为它们帮助您控制哪些函数会被公开。

更多细节:如果没有名称,您就无法在除了创建它的确切位置之外的任何地方重新分配或篡改它。一个好的经验法则是,如果您不需要在任何其他地方重复使用此函数,那么最好考虑使用匿名函数,以防止在任何地方被篡改。

例如: 如果您正在处理一个有很多人的大项目,如果您在较大的函数内部有一个函数并将其命名为某些内容,那么意味着与您一起工作并编辑较大函数代码的任何人都可以随时对该较小函数进行更改。如果您将函数命名为“add”,并且有人重新分配标签“add”从函数到数字,则整个系统就会崩溃!

PS-我知道这是一个非常古老的帖子,但有一个更简单的答案,我希望有人能够像我一样作为初学者寻找答案时以这种方式表达 - 我希望你愿意复活一个旧的线程!


你所提出的大部分内容都可以通过 letconst 作用域来实现。在2021年,使用IIFEs的好理由并不多。请参见:https://levelup.gitconnected.com/why-its-time-to-stop-using-javascript-iifes-b62602f25bfc - Chris Perry
嗨,克里斯!感谢你的反馈。匿名函数并不等同于IIFE,但这篇文章是一个很好的分享。对于初学者来说,了解匿名函数如何工作以及它们为什么有用仍然很重要,特别是如果他们还没有学习模块的话。 - Lindsay Baird
1
感谢您指出这一点,Lindsay - 您当然是正确的。同意这些都是值得学习的重要概念。为了完整起见,这里有另一篇文章介绍了这两个概念之间的差异和相似之处:https://medium.com/@DaphneWatson/anonymous-functions-and-iife-immediately-invoked-function-expressions-with-javascript-69d3f554fca2 - Chris Perry

3

使用命名函数可读性更好,它们还可以像下面的示例一样进行自引用。

(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或者你来决定。


1

好的,为了表述清楚我的论点,以下内容在我看来都是匿名函数/函数表达式:

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'); })()

优点:

  • 它可以减少一些冗余代码,特别是在递归函数中(在这种情况下,您仍然可以(实际上应该)使用命名引用,如最后一个示例所示),并清楚地表明函数只在此处触发。

  • 代码可读性更高:在将匿名函数分配为方法的对象文字示例中,如果在代码中添加更多查找逻辑的位置,那么整个对象文字的目的就是将一些相关功能放置在同一方便引用的位置,这样做就有点傻了。但是,在构造函数中声明公共方法时,我倾向于内联定义标记函数,然后将其分配为this.sameFuncName的引用。它使我能够在不使用“this.”冗余的情况下在内部使用相同的方法,并使定义顺序成为彼此调用时的非关注点。

  • 有助于避免不必要的全局命名空间污染-但是,内部命名空间不应该被广泛填充或由多个团队同时处理,因此我认为这个论点有点愚蠢。

  • 当设置短事件处理程序时,我同意使用内联回调。寻找1-5行函数是愚蠢的,特别是由于JS和函数提升,定义可能出现在任何地方,甚至不在同一个文件中。这可能是意外发生的,而不会破坏任何内容,而且您并不总是控制这些内容。事件始终导致回调函数被触发。没有理由添加更多链接到您需要扫描的名称链中,以便在大型代码库中反向工程简单的事件处理程序,并且可以通过将事件触发器本身抽象为在调试模式下记录有用信息并触发触发器的方法来解决堆栈跟踪问题。我实际上正在使用这种方式构建整个接口。

  • 当您希望函数定义的顺序很重要时非常有用。有时,您希望确保默认函数是您认为的那样,直到代码的某个点,在该点上重新定义它是可以的。或者,当依赖项被洗牌时,您希望断开更明显。

缺点:

  • 匿名函数无法利用函数提升。这是一个重大的区别。我倾向于充分利用提升来定义自己明确命名的函数和对象构造函数,将它们放在底部,然后在顶部处理对象定义和主循环类型的内容。我发现当你良好地命名变量并在需要时查找细节时,代码更容易阅读。在强烈事件驱动的界面中,强制规定可用性的严格顺序可能会让你感到困扰,而提升则可以成为一个巨大的优势。提升也有其自身的注意事项(如循环引用潜力),但是当正确使用时,它是组织和使代码易读的非常有用的工具。

  • 易读性/调试。绝对有时会过度使用,这可能会使调试和代码易读性成为麻烦。例如,严重依赖JQ的代码库如果不以明智的方式封装接近不可避免的匿名函数和大量超载的$ soup参数,则可能难以阅读和调试。例如,JQuery的hover方法就是匿名函数过度使用的经典例子,因为当你将两个匿名函数放入其中时,很容易让初学者认为它是一个标准的事件监听器分配方法,而不是一种方法过载以为一个或两个事件分配处理程序。 $(this).hover(onMouseOver, onMouseOut) 比两个匿名函数更清晰。


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