65

我是 JavaScript 新手。我了解该语言的许多概念,我一直在阅读原型继承模型,并且我正在用越来越多的交互式前端内容来刺激我的口哨。这是一种有趣的语言,但我总是对许多非平凡交互模型中典型的回调意大利面条感到厌烦。

我一直觉得奇怪的是,尽管 JavaScript 嵌套回调嵌套是可读性的噩梦,但我在许多示例和教程中很少看到的一件事是使用预定义的命名函数作为回调参数。我白天是一名 Java 程序员,摒弃了对代码单元的 Enterprise-y 名称的刻板印象,我喜欢使用一种具有强大功能 IDE 选择的语言的一件事就是使用有意义的、如果很长,名称可以使代码的意图和含义更加清晰,而不会使实际生产变得更加困难。那么为什么在编写 JavaScript 代码时不使用相同的方法呢?

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

优点:

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

缺点:

  • 这是可怕的和不可读的
  • 当您将坚果嵌套在回调沼泽的深处时,它会增加混乱(公平地说,这可能意味着您一开始就编写了结构不佳的代码,但这很常见)。
  • 对于没有功能背景的人来说,这可能是一个奇怪的概念

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

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

4

6 回答 6

59

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

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

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

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

于 2012-04-22T23:58:49.390 回答
13

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

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

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

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

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

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

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

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

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

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

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

于 2012-04-23T14:55:33.820 回答
10

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

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

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

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

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

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

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

于 2016-11-20T16:54:52.667 回答
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 或者嗨,我的名字是 ???? 你选。

于 2014-02-11T16:19:40.183 回答
3

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

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

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

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

于 2021-06-09T18:59:32.167 回答
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'); })()

优点:

  • 它可以减少一些麻烦,特别是在递归函数中(你可以(实际上应该因为 arguments.callee 已被弃用)仍然在内部使用最后一个示例的命名引用),并清楚地表明该函数只会在这个函数中触发地方。

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

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

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

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

缺点:

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

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

于 2012-05-02T22:08:58.210 回答