在 JavaScript 事件代码中对回调和参数使用匿名函数而不是命名函数有什么好处?

我是 JavaScript 的新手。我理解语言的许多概念,我一直在阅读关于原型继承模型的文章,并且我正在使用越来越多的交互式前端工具。这是一种有趣的语言,但是我总是对许多非平凡交互模型的典型回调意大利面感到有点厌烦。

对我来说,有一件事总是很奇怪,那就是尽管可读性是一个由 JavaScript 嵌套回调组成的噩梦,但是在很多示例和教程中我很少看到的一件事是使用预定义的命名函数作为回调参数。我白天是一个 Java 程序员,而 抛弃关于代码单元的 Enterprise-y 名称的刻板印象是我在一个拥有众多功能强大的 IDE 的语言中工作的乐趣之一,使用有意义的,如果很长的名称,可以使代码的意图和意义更加清晰,而不会使它更难以实际生产。那么为什么不在编写 JavaScript 代码时使用相同的方法呢?

仔细想想,我可以提出支持和反对这个想法的论点,但是我对语言的天真和新鲜使我无法得出任何结论,为什么这在技术层面上是好的。

优点:

  • 灵活性。带有回调参数的异步函数可以通过许多不同的代码路径中的一个来访问,而且可能需要编写一个命名函数来说明每一个可能的边缘情况。
  • 速度。它在黑客心态中扮演着重要的角色。把东西固定在它上面,直到它工作起来。
  • 其他人都这么做
  • 更小的文件大小,即使是微不足道的,但每一个位计数的网络。
  • 更简单的 AST?我会假设匿名函数是在运行时生成的,因此 JIT 不会在将名称映射到指令方面做太多工作,但我现在只是猜测。
  • 调度更快? 这个也不确定,再猜一次。

缺点:

  • 太可怕了,读不懂
  • 当你在回调函数的沼泽深处嵌套时,它增加了混乱(公平地说,这可能意味着你开始编写构造不良的代码,但这是很常见的)。
  • 对于没有功能性背景的人来说,理解这个概念可能是一个奇怪的概念

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

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

46968 次浏览

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

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

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

我想我要补充的是,如果一个回调开始获得超过15-20行的长度,并且它不需要直接访问父作用域中的变量,那么我会给它一个名称,然后将它分解成在其他地方声明的自己的命名函数。这里肯定存在一个可读性点,在这个点上,如果将一个非平凡的函数放在它自己的命名单元中,那么它会变得更加易于维护。但是,我最终得到的大多数回调都没有那么长,而且我发现将它们保持在内联状态更具可读性。

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

我将在其他地方使用这个函数吗?

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

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

然而,代码的可维护性也非常重要。每种情况都不一样。如果一开始就没有编写很多这样的函数,那么以任何一种方式编写都没有坏处。这完全取决于你的喜好。

关于名字的另一个注释。养成定义长名称的习惯将真正损害您的文件大小。以下面的例子为例。

假设这两个函数的作用相同:

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


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

第二个命令准确地告诉你它在名称中的作用。第一个更加模棱两可。但是,名称中有17个不同的字符。假设这个函数在整个代码中被调用了8次,这就是你的代码不需要的 额外的153个字节。虽然不是很大,但如果是一种习惯的话,推断出10秒甚至100秒的功能很容易就意味着下载量有几 KB 的差异。

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

好吧,为了清楚我的论点,下面是我书中所有的匿名函数/函数表达式:

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

优点:

  • 它可以减少一点儿 cruft,特别是在递归函数中(在递归函数中,您可以(实际上应该因为 arguments.callee 已经被弃用)仍然在内部使用每个最后一个示例的命名引用) ,并且明确表示函数只在这一个地方触发。

  • 代码易读性胜出: 在一个将匿名函数分配为方法的对象文字的例子中,如果在代码中添加更多的位置来搜索和查找逻辑,那将是愚蠢的,因为该对象文字的整个目的是将一些相关的功能放在同一个方便引用的位置。但是,在构造函数中声明公共方法时,我倾向于在内联中定义带标签的函数,然后将其分配为 this.sameFuncName 的引用。它允许我在内部使用相同的方法,而不需要‘ this’。当他们互相称呼对方的时候,把定义的顺序变成一个无关紧要的问题。

  • 对于避免不必要的全局名称空间污染非常有用——但是,内部名称空间不应该由多个团队同时进行广泛的填充或处理,因此这种说法在我看来有点愚蠢。

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

  • 当你需要函数定义的顺序时很有用。有时候你想确定一个默认函数就是你想的那样,直到代码中的某个特定点可以重新定义它。或者,当依赖项被洗牌时,您希望破坏更加明显。

缺点:

  • 匿名函数不能利用函数提升的优势。这是一个很大的区别。我倾向于在底部定义我自己的显式命名的函数和对象构造函数,然后在顶部定义对象和主循环类型。我发现,当您很好地命名 var 时,它使代码更容易阅读,并且在 ctrl-Fing 之前,只有当它们对您很重要时,才能对正在发生的事情有一个广泛的了解。在严重事件驱动的界面中,提升也是一个巨大的好处,因为它对可用的内容进行了严格的排序,这可能会给您带来麻烦。吊装有它自己的警告(如循环参考电位) ,但它是一个非常有用的工具,组织和使代码易读时使用的权利。

  • 可读性/调试。毫无疑问,它们有时会被过度使用,这会让调试和代码的易读性变得非常麻烦。例如,严重依赖于 JQ 的代码库,如果你不能以一种合理的方式封装那些几乎不可避免的、非常繁重且大量超载的 $汤的参数,那么读取和调试代码库就会成为一个严重的 PITA。例如,JQuery 的 hover 方法就是一个典型的当你放入两个 anon function 时过度使用 anon function 的例子,因为第一次使用的人很容易认为它是一个标准的事件侦听器分配方法,而不是一个重载的方法来为一个或两个事件分配处理程序。$(this).hover(onMouseOver, onMouseOut)比两个匿名函数清晰得多。

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

(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 或者嗨,我的名字是? ? ? ? 你选吧。

有点晚了,但有些还没有提到方面的功能,匿名或其他..。

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

Anon 函数对调试器也是匿名的。(切!)因此,调试器堆栈跟踪通常只显示问号或类似的内容,这使得在设置了多个断点时它不太有用。你触发了断点,但是发现自己正在向上/向下滚动调试窗口来找出你在程序中的位置,因为问号函数根本不能做到这一点!

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

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

将 Anon 函数分配给一个 var,然后将 var 作为回调传递(而不是内联) ,这样可以提高它们的可读性。例如: Var funky = function (...){ ... } ; JQuery (’# otis’) . click (funky) ;

使用上述方法,您可以在父函数的顶部对几个匿名函数进行分组,然后在这个顶部之下,顺序语句的主要部分将变得更紧密地分组,并且更容易阅读。

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

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

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

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