为什么Array.prototype.forEach不能被链接?

时间:2015-03-24 08:37:56

标签: javascript arrays functional-programming

我今天了解到forEach()返回undefined。多么浪费!

如果它返回原始数组,那么在不破坏任何现有代码的情况下它将更加灵活。是否有任何理由forEach返回undefined

无论如何将forEach链接到其他方法,例如map& filter

例如:

var obj = someThing.keys()
.filter(someFilter)
.forEach(passToAnotherObject)
.map(transformKeys)
.reduce(reduction)

无法正常工作,因为forEach并不想玩得很好,要求您再次在forEach之前运行所有方法,以使对象处于所需的状态forEach

2 个答案:

答案 0 :(得分:22)

您希望通过method cascading将其称为method chaining。简要描述一下:

  1. 方法链接是指方法返回的对象具有您立即调用的另一个方法。例如,使用jQuery:

    $("#person")
        .slideDown("slow")
        .addClass("grouped")
        .css("margin-left", "11px");
    
  2. 方法级联是指在同一对象上调用多个方法。例如,在某些语言中,您可以这样做:

    foo
        ..bar()
        ..baz();
    

    这相当于JavaScript中的以下内容:

    foo.bar();
    foo.baz();
    
  3. JavaScript没有任何特殊的方法级联语法。但是,如果第一个方法调用返回this,则可以使用方法链接模拟方法级联。例如,在以下代码中bar返回this(即foo),则链接等同于级联:

    foo
        .bar()
        .baz();
    

    某些方法(如filtermap)是可链接的,但不能级联,因为它们返回一个新数组,但不返回原始数组。

    另一方面,forEach函数不可链接,因为它不会返回新对象。现在,问题是forEach是否应该是可级联的。

    目前,forEach不可级联。但是,这不是一个真正的问题,因为您可以简单地将中间数组的结果保存在变量中并稍后使用:

    var arr = someThing.keys()
        .filter(someFilter);
    
    arr.forEach(passToAnotherObject);
    
    var obj = arr
        .map(transformKeys)
        .reduce(reduction);
    

    是的,这个解决方案看起来比您想要的解决方案更糟糕。但是,由于以下几个原因,我比您的代码更喜欢它:

    1. 这是一致的,因为可链接方法不与可级联方法混合。因此,它促进了编程的功能风格(即没有副作用的编程)。

      级联本质上是一种有效的操作,因为您正在调用方法并忽略结果。因此,您正在调用该操作的副作用,而不是其结果。

      另一方面,像mapfilter这样的可链接函数没有任何副作用(如果它们的输入函数没有任何副作用)。它们仅用于结果。

      在我的拙见中,将mapfilter之类的可链接方法与可forEach等可级联函数(如果它是可级联的)混合在一起是亵渎神灵,因为它会在其他方面引入副作用转化

    2. 这是明确的。正如The Zen of Python告诉我们的那样,“明确比隐含更好。”方法级联只是语法糖。这是隐含的,而且需要付出代价。成本很复杂。

      现在,您可能会认为我的代码看起来比您的代码更复杂。如果是这样,你将以封面来判断这本书。在他们着名的论文Out of the Tar Pit中,作者Ben Moseley和Peter Marks描述了不同类型的软件复杂性。

      其列表中第二大软件复杂性是由明确关注控制流引起的复杂性。例如:

      var obj = someThing.keys()
          .filter(someFilter)
          .forEach(passToAnotherObject)
          .map(transformKeys)
          .reduce(reduction);
      

      上述程序明确涉及控制流程,因为您明确指出.forEach(passToAnotherObject)应该在.map(transformKeys)之前发生,即使它不会对整体转换产生任何影响。

      事实上,你可以完全从等式中删除它,它不会有任何区别:

      var obj = someThing.keys()
          .filter(someFilter)
          .map(transformKeys)
          .reduce(reduction);
      

      这表明.forEach(passToAnotherObject)首先没有任何业务。由于它是一个有效的操作,它应该与纯代码分开。

      当你像上面那样明确地编写它时,不仅可以将纯代码与副作用代码分开,而且还可以选择何时评估每个计算。例如:

      var arr = someThing.keys()
          .filter(someFilter);
      
      var obj = arr
          .map(transformKeys)
          .reduce(reduction);
      
      arr.forEach(passToAnotherObject); // evaluate after pure computation
      

      是的,您仍然明确关注控制流程。但是,至少现在您知道.forEach(passToAnotherObject)与其他转换无关。

      因此,您已经消除了一些(但不是全部)由于明确关注控制流而导致的复杂性。

    3. 由于这些原因,我认为forEach的当前实现实际上是有益的,因为它会阻止您编写由于明确关注控制流而引入复杂性的代码。

      根据我以前在BrowserStack工作时的个人经验,我明白对控制流的明确关注是大型软件应用程序中的一个大问题。这确实是一个现实世界的问题。

      编写复杂代码很容易,因为复杂代码通常是较短(隐式)代码。所以它总是很想在纯计算中放入像forEach这样的副作用函数,因为它需要更少的代码重构。

      然而,从长远来看,它会使您的程序更加复杂。想想当你退出你工作的公司并且其他人必须维护你的代码时,几年后会发生什么。您的代码现在看起来像:

      var obj = someThing.keys()
          .filter(someFilter)
          .forEach(passToAnotherObject)
          .forEach(doSomething)
          .map(transformKeys)
          .forEach(doSomethingElse)
          .reduce(reduction);
      

      现在,阅读代码的人必须假设链中的所有其他forEach方法都是必不可少的,需要额外的工作来理解每个函数的作用,自己弄清楚这些额外的{{1} }方法对于计算forEach并不是必不可少的,将它们从代码的心理模型中消除,只关注基本部分。

      您的程序中添加了许多不必要的复杂性,并且您认为它使您的程序更加简单。

答案 1 :(得分:1)

很容易实现可链接的forEach函数:

Array.prototype.forEachChain = function () {
    this.forEach(...arguments);
    return this;
};

const arr = [1,2,3,4];

const dbl = (v, i, a) => {
    a[i] = 2 * v;
};

arr.forEachChain(dbl).forEachChain(dbl);

console.log(arr); // [4,8,12,16]