为什么Array.prototype.forEach不能上链?

Why can Array.prototype.forEach not be chained?

今天得知forEach()returnsundefined。太浪费了!

如果它返回原始数组,在不破坏任何现有代码的情况下会更加灵活。有什么理由吗forEachreturnsundefined.

是否可以将 forEachmapfilter 等其他方法链接起来?

例如:

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

不会工作,因为 forEach 不想玩好,要求您再次 运行 forEach 之前的所有方法以获取状态中的对象forEach.

需要

你想要的是method cascading via method chaining。简要描述它们:

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

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

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

    相当于JavaScript中的以下内容:

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

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

foo
    .bar()
    .baz();

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

另一方面,forEach 函数不可链接,因为它不是 return 新对象。现在,问题出现了 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) 与其他转换无关。

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

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

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

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

但是,在较长的 运行 中,它会使您的程序更加复杂。想一想几年后当你离开你工作的公司而其他人必须维护你的代码时会发生什么。您的代码现在看起来像:

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

阅读你代码的人现在必须假设你的链中所有额外的 forEach 方法都是必不可少的,投入额外的工作来理解每个函数的作用,自己弄清楚这些额外的 forEach 方法对于计算 obj 不是必不可少的,请将它们从她的代码心智模型中删除,只专注于重要部分。

这为您的程序增加了很多不必要的复杂性,您认为这会使您的程序更简单。

实现可链式 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]