详解Array.reduce函数
2019年02月11日

减少你对Array.reduce()的恐惧!

如果你花费大部分的时间在使用JavaScript工作,你可能会熟悉JavaScript的数组Array并使用过它内建的一些方法,例如map,filter和forEach等等。有一个方法你可能不是很熟悉(特别是刚刚接触JavaScript的开发人员),它就是reduce,也是所有数组方法中我最喜欢的。
不得不承认,过去并不是这个样子,有很长一段时间我没有学习如何熟练使用reduce。直到JavaScript这个领域中迎来了Redux和函数式编程,我才坐下来好好的思考它。在学习之后,才了解Reduce是多么有用的一个函数,也是现在我为什么甚至没有思考过就经常使用它的原因。让我告诉你为什么我这么喜欢它。

关于数组的思考

数组是一个很好的数据模型,主要因为它们是可遍历的。这意味这你能用reduce(以及map,filter或者使用传统的for循环)去移动数组中的每一项,并对其执行一些操作。
如果你仔细观察过开发的项目,就会发现项目的UI通常是通过列表的形式展示出来,并且也确实有很多的Apps是这样做的。想想你的Twitter或是Facebook的消息动态,实质上就是很多对象的数组。To-do App也是同样的列表。再就是一个聊天的App中:传送的信息,对话的列表都表现的像一个数组。

通常的开发模式

当你准备展示像列表一样的应用数据,你通常需要从每一项中获取一些数据或者展示经过简单修改后的数据。举例来讲,我们有这样一组Users数据,它是这个样子的:

const users = [
    {
        firstName: 'Bob',
        lastName: 'Doe',
        age: 37,
    }, {
        firstName: 'Rita',
        lastName: 'Smith',
        age: 21,
    }, {
        firstName: 'Rick',
        lastName: 'Fish',
        age: 28,
    }, {
        firstName: 'Betty',
        lastName: 'Bird',
        age: 44,
    }, {
        firstName: 'Joe',
        lastName: 'Grover',
        age: 22,
    }, {
        firstName: 'Jill',
        lastName: 'Pill',
        age: 19,
    }, {
        firstName: 'Sam',
        lastName: 'Smith',
        age: 22,
    }
    //  模拟一个简单的列表数据
];

在一些使用场景下,我们需要一个新的具有所有user全称的数组(它的姓 + ‘ ‘ + 它的名),并且要求user年龄在20岁,全称的名字超过10个字母。不要问为什么我们需要这么一个奇怪的数据,PM需要它,我们就只能去提供它。
使用函数式编程解决这个问题可能是下面这个样子:

const twentySomethingsLongFullNames = users
 
      //  首先我们过滤初年龄在20岁的人
      .filter(user => user.age >= 20 && user.age < 30)
 
      //  然后将每一项的名和姓拼接到一起
      .map(user => `${user.firstName} ${user.lastName}`)
 
      //  最后过滤掉全名字符数小于9的
      .filter(fullName => fullName.length >= 10);

或许你想独立的测试这些函数,那让我们将功能函数抽象出来,让它变得更具有可读性

const isInTwenties = user => user.age >= 20 && user.age < 30;
const makeFullName = user => `${user.firstName} ${user.lastName}`;
const isAtLeastTenChars = fullName => fullName.length >= 10;
 
const twentySomethingsLongFullNames = users
                                        .filter(isInTwenties)
                                        .map(makeFullName)
                                        .filter(isAtLeastTenChars);

以这种方式处理这个问题非常优雅,并且对于数据少的数组,这绝对是我推荐去处理解决问题的方式。每个函数都可以被独立测试,这种写法真的很不错。然而,同样重要的是这不是唯一的方式去完成这个功能,当你遇到相当大规模的数据,处理这样的问题时,考虑其他的方式可能是更好的。

那reduce函数就是为我们而来。

减少数组迭代次数

在上述提供的方法中,我们总共遍历了3次数组(更确切的说,每运行一个函数都会返回一个修改后的数组)。现在面对大多数情况,这么做是可以的,因为通常我们不会去处理大量的数据,所以用几次遍历可能它也不会花费很长时间去执行它们。
在链式调用里,每一次使用filter,可能会返回一个更小的数组,所以遍历时,它不会像原始数组那么长。但它仍然不是理想的方式去减少迭代次数。

假如我们开发的应用非常成功,并且我们将要处理类似于Google/Facebook/Hooli这种大规模的数据。在这种情况下,我们想要减少过多的运算数量。当三次遍历数组完成的事情我们可以一次遍历解。那对我们来说,这一定是显著的性能优化。
让我们给出一个例子,在这个例子中,我们使用reduce函数,而且使用上面命名过的三个函数(因为我们已经为它们写过测试,并且我们不想重写或者移除它们)

const isInTwenties = user => user.age >= 20 && user.age < 30;
const makeFullName = user => `${user.firstName} ${user.lastName}`;
const isAtLeastTenChars = fullName => fullName.length >= 10;
 
const twentySomethingsLongFullNames = users.reduce(
  //  第一个参数是我们的reducer函数 
  (accumulator, user) => {
      const fullName = makeFullName(user);
      if (isInTwenties(user) && isAtLeastTenChars(fullName)) {
          accumulator.push(fullName);
      }
      //  Always return the accumulator (for the next iteration)
      return accumulator
  },
  //  第二个参数(可选)是初始值
  []
);

让我们分解来看发生了什么。reduce函数接受两个参数,第一个是我们的reducer回调函数,第二个是初始值。
然后reducer回调函数本身接受几个参数,第一个参数是accumulator 累加器,第二个是数组中的item,第三个参数是该项的索引,最后一个参数是原始数组的引用。
accumulator累加器是在你操作数组时需要积累的值。当reducer函数在数组第一项上运行时,初始值参数会被赋值给accumulator。如果你没有提供一个初始值,reduce实际上是从index==1的项(数组的第二项)开始运行,此accumulator累加器的值将会是数组的第一项。
我喜欢reduce的原因就像它的名字一样介绍了它做了什么–你想要reduce一个数组进而从数组中得到你想要的数据。在我们的例子中,我们想要的结果是一个新的数组(具有全称的users数组)。但是你使用reduce的结果不一定必须是另一个数组。你可以reduce生成一个新的对象,一个原始的类型,例如一个bool值,一个数字,或者是任何你想要的一个值。

减少迭代次数有什么好处呢?

在例子中,你可能会好奇我在前文提及到的性能。我测试了这个通过生成一个巨大的users数组(100000个元素)。然后我做了粗滤的 console.time 去检查链式调用三个函数与使用reduce函数一次遍历哪个是更快的。正如期待的那样,一次遍历的结果是很快的(至少快了三倍)
再一次强调,一次便利的版本并不是绝对的更好的选择,因为filter-map-filter的版本是更具有可读性的。但是如果你是那种可以在尽可能写出具有优秀性能的代码中获得乐趣的程序员,又或者如果你注意到你的App在大规模数据中有点慢,也许在大数据的情况下减少遍历次数是值得的。

优雅,展示另一个例子

或许你并不会去处理大量的数据,所以你可能仍然不了解reduce的能力。那让我们赶快进一步了解如何使用reduce函数去扩展我们自己的Array.find函数。find函数是很方便的,但是因为他是比较新的特性,浏览器支持并不是很完善,所以让我们使用reduce函数替代它。第一步看起来是这样子的:

const fruits = [
  { name: 'apples', quantity: 2 },
  { name: 'bananas', quantity: 0 },
  { name: 'cherries', quantity: 5 }
];

const thisShitIsBananas = fruits.reduce((accumulator, fruit) => {
    return accumulator
});

第一条规则就是总是要返回你的accumulator(或者至少返回什么东西)。无论你从你的reducer回调函数中返回什么,它都会被当作数组下一项中回调函数的accumulator,所以当你遍历到数组的最后一项时,如果你什么都不返回,你将会得到undefined
下一步,我们将修改我的accumulator(返回我们想要的Item)。可以像下面这么做:

const thisShitIsBananas = fruits.reduce((accumulator, fruit) => {
    if (fruit.name === 'bananas') return fruit;
    return accumulator
});

现在我们可以拿到数组中的’bananas’,我们会返回数组中的那一项,它将会成为新的accumulator,并且对于数组的其他项,我们仅仅是返回accumulator,直到结束,它将会返回最终的结果。但这是非常糟糕的代码,所以我自己进行了整理,像下面这样

//  arrayFind 接受一个数组,并返回一个函数
//  返回的函数接受finder函数
const arrayFind = arr => fn => arr.reduce((acc, item, index) => {
    //  我们将item与index传递给finder函数
    if (fn(item, index)) return item;
    return acc;
});
//  创建一个finder函数去查找我们的水果
const fruitFinder = arrayFind(fruits);
//  现在我们能传递一个简单的finder 给 fruitFinder
//  这个函数就是上述的'fn'函数
const thisShitIsBananas = fruitFinder(fruit => fruit.name === 'bananas');

以这个方式,我们的arrayFind接受一个数组,然后通过传递一个简单的匹配函数,它返回的函数就会像实际上Array.find那样工作
上述我所做的只是Array.find一个粗略的实现版本–实际版本将会返回数组中第一个匹配到的项,然而我们写的函数实际上会返回最后一个数组中最后一个匹配到的项(所以,在这个例子中,如果有超过一个’bananas’,将会返回最后一个)。这不是很完美,你可以自己私下里自己解决这个问题。

最后一个例子:处理数组生成一个简单的字符串

最后一个例子。我们用第一例子中的users数据作为例子, 这一次我们的产品经理想要所有users的名字拼成字符串,每人的全称后需要跟一个换行(可能是写到md文件或者什么东西,谁知道呢)
我首先会展示给你一个原始的开发方式,然后接下来我将展示我怎么使用reduce函数完成它,以一种更简单的方式。

/*  原始写法  */
let everyonesName = '';
users.forEach(user => {
    everyonesName += `${user.firstName} ${user.lastName}\n`;
});
/*  优化后的写法  */
const everyonesName = users.map(
    user => `${user.firstName} ${user.lastName}\n`
).join('');
/*  使用reducer后最好的写法  */
const everyonesName = users.reduce(
    (acc, user) => `${acc}${user.firstName} ${user.lastName}\n`,
    ''
);

第一种方式,我们经常修改函数内的ereryoneName变量。这是不好的。第二个方式不是很差,但是map函数会返回另一个数组,join函数将不得不遍历它去生成最终的字符串
与使用reduce的方式进行对比,我们只遍历了一次,并且返回一个新的字符串,它连接了数组中当前用户的全名与存在的名字的字符串(通过累加器),最终的结果字符串会在reduce最后一次函数执行后返回。

需要记住的是,我们的初始值是一个空字符串,因为我们想,当我们拼接数组中第一个名字的时候,累加器应该是空值。如果我们正在构建表述输出一个markdown文件,并且想要一个’User List’或者其他什么东西的标题,我们能简单使用初始值作为替代,并且它将会提前接入到users列表中。所以说,re du ce真的很方便。

现在我希望你在使用reduce会更熟悉一些。即便你不会使用它像其他的数组函数那样多,但它仍然是一个重要的工具以及对数据的处理方式。如果你想练习有对reduce有一个更好的掌握,我推荐你去尝试去实现一些数组其他的方法通过使用reduce,Just for fun。

本文转载自:https://blog.csdn.net/zhendong9860/article/details/74908062