24小时联系电话:15828505243

不使用JS匿名函数的三个理由

如果你不知道匿名函数是什么,这里有一个引语:

匿名函数是一种在运行时动态声明的函数。它们之所以被称为匿名函数是因为不同于普通函数,它们并没有函数名。 — Helen Emerson, Helephant.com

匿名函数形式如下:

  1. function () { ... code ... }
  2. OR
  3. (args) => { ... code .. }

今天我尝试让大家理解只有在绝对需要的情况下才使用匿名函数的想法。匿名函数不应该是首选,而且你自己也应该知道为什么使用它。当理解这种想法之后,你的代码会变得更简洁,更容易维护,并且更容易跟踪bug。

先从避免使用匿名函数的三个理由开始:

无论你多么擅长写代码,出现错误也是不可避免的。有时候,这些错误很容易被查出,有时候并不容易。

如果你知道这些错误来自哪里,那么错误会很容易被查出来。为了容易查出错误,我们使用这个被叫做堆栈轨迹的工具。如果你不了解堆栈轨迹,goole给出了很棒的介绍。

假设现在有一个非常简单的项目:

  1. function start () {
  2.  (function middle () {
  3.    (function end () {
  4.      console.lg('test');
  5.     })()
  6.   })()
  7. }

上面代码里面有一个非常愚蠢的错误,拼写错误(console.log)。在小项目里面,这个拼写错误不是什么大问题。如果这是一个有非常多模块非常大的项目一小段,问题就大了。假设这个愚蠢的错误不是你犯的,那么新来的初级工程师将会在他休假之前把这个错误提交到代码库!

现在,我们必须追查。 使用我们精心命名的函数,我们得到如下的堆栈跟踪:

谢谢你命名你的函数,初级开发者们! 现在我们可以轻松地追踪到这个bug。

但是..一旦我们解决了这个问题,就会发现还有另一个bug。 这次是一位更资深的开发人员介绍的。这个人知道lambdas(匿名函数),并在代码中大量使用它们。 结果他们偶然发现了一个bug,我们的工作就是追踪它。

下面是代码:

  1. (function () {
  2.  (function () {
  3.    (function () {
  4.      console.lg('test');
  5.     })();
  6.   })();
  7. })();

吃不吃惊,这名开发者也忘记了如何拼写console.log了!这也太巧合了吧!令人感到遗憾的是,他们都没有命名他们的函数。

那么控制台会输出什么呢?

好吧,我们至少还有行号,对吧?在这个例子中,看起来我们有大约7行代码。如果我们处理一大段代码会如何呢?比如一万行代码?行号的跨度如此之大该怎么办呢?如果代码被折叠后有没有一个代码地图文件,那么对行号的渲染是不是根本就是没有什么用了呢?

我想对这些问题的回答相当简单,答案就是:想这些会让你一整天都会过的相当糟心。

可读性

咦,我听说你还不信。你仍旧对你的匿名函数恋恋不舍,并且还从未发生过bug。我的错,你的代码是完整的。但是让我们看看这个!

看看下面两段代码:

  1. function initiate (arguments) {
  2.   return new Promise((resolve, reject) => {
  3.     try {
  4.       if (arguments) {
  5.          return resolve(true);
  6.       }
  7.       return resolve(false);
  8.     } catch (e) {
  9.       reject(e);
  10.     }
  11.   });
  12. }
  13. initiate(true)
  14.   .then(res => {
  15.         if (res) {
  16.           doSomethingElse();
  17.         } else {
  18.           doSomething();
  19.         }
  20.   ).catch(e => {
  21.             logError(e.message);
  22.             restartApp();
  23.           }
  24.   );

这是一个非常不正常的例子,但是我相信你已经明白我要说什么了。我们反悔了一个promise方法,我们用这个promise对象/方法处理不同的响应。

你也许会认为几段代码读起来并不难,但我认为它们可以变得更好!

如果我们去掉所有的匿名函数会怎样呢?

  1. function initiate (arguments) {
  2.   return new Promise(checkForArguments);
  3. }
  4. function checkForArguments (resolve, reject) {
  5.   try {
  6.     if (arguments) {
  7.      return resolve(true);
  8.     }
  9.     return resolve(false);
  10.   } catch (e) {
  11.     reject(e);
  12.   }
  13. }
  14. function evaluateRes (res) {
  15.   if (res) {
  16.     doSomethingElse();
  17.   } else {
  18.     doSomething();
  19.   }
  20. }
  21. function handleError (e) {
  22.   logError(e.message);
  23.   restartApp();
  24. }
  25. initiate(true)
  26.   .then(evaluateRes)
  27.   .catch(handleError);

好,先讲清楚:这部分代码更长,但我不认为只是增加了可读性!我们精心命名的函数与匿名函数不一样,只要我们一看到它们的名字就知道它们的功能是什么。这避免了在评估代码时出现心理障碍。

这也有助于分清楚其中的关系。与创建一个方法、将其传递、然后运行逻辑不同,在第二个例子中的参数被给到了then,catch只是指向了发生所有事情的函数。

关于更具有可读性,我没有什么再能说服你的了。但是也许你还没被说服的话,我可以试一下最后的论据。

可重用性

你注意到上一个例子了吗?上个例子中的函数的使用范围从参数和初始化函数,变为让所有函数都能使用。

当你使用匿名函数时这些函数很难在你的应用程序内重复使用。

可重用性将不复存在,最终你会一遍又一遍地写重复的代码。正如我们所见的,代码写的越少引入的Bug就越少,用户必须加载的内容就越少。所有人都会因此获益!

相反的,命名函数可以全局使用,而不需要像变量一样到处传递。你的代码的可重用性会更好,

匿名函数有可取的地方吗?

有。虽然很不愿意承认,但有时候使用匿名函数是最好的选择。

  1. const stuff = [
  2.   { hide: truename'justin' },
  3.   { hide: falsename'lauren' },
  4.   { hide: falsename'max' },
  5. ];
  6. const filteredStuff = stuff.filter(s => !s.hide);

上边代码中的匿名函数s => !s.hide非常简单,即使不能在别的地方使用也不会对别人有任何影响,而且也可以在stuff.filter中显示出堆栈调用。如果想要重用这段代码,最好重用整段代码:

  1. function filterByHide (array) {
  2.   return array.filter(item => !item.hide);
  3. }

有时你想把你所有的代码封装到匿名函数中,以保证全局范围不会被污染。

  1. (() => {
  2.  ... your code here ...
  3. })();

在栈空间中拥有一个顶级的匿名函数真得不会有什么错误。没有代码重用是痛苦的,因为完整的目的是保持方法内含。

我确定这会有其他好的用法,请在评论中自由分享之!