0
点赞
收藏
分享

微信扫一扫

【JS】379- 教你玩转数组 reduce

reduce 是数组迭代器(https://jrsinclair.com/articles/2017/javascript-without-loops/)里的瑞士军刀。它强大到您可以使用它去构建大多数其他数组迭代器方法,例如 ​.map()​​、 ​.filter()​​ 及 ​.flatMap()​。在这篇文章中,我们将带你用它来做一些更有趣的事情。阅读前,我们需要您对数组迭代器方法有一定的了解。

Reduce​是迄今为止发现的最通用的功能之一Eric Elliott

使用 ​reduce​​ 做加法乘法还可以,可一旦要超出现有基础示例,人们就会觉着有些困难。更复杂的字符串什么的,可能就不行了。使用 ​reduce​ 做和数字以外的事情,总会觉着有些怪怪的。

为什么 ​​reduce()​​ 会让人觉着很复杂?

我猜测主要有两个原因。第一个是,我们更愿意教别人使用 ​.map()​​ 和 ​.filter()​​ 却不教 ​reduce()​​。​reduce()​​ 和 ​map()​​ 或者 ​.filter()​​ 用起来的感觉非常不同。每个不一样的初始值,经过 ​reduce​ 之后,都会有不同的结果。类似将当前数组元素进行累加得到的值。

第二个原因是我们如何去教人们使用 ​.reduce()​。下面这样的教程并不少见:

  1. function add(a, b) {
  2. return a + b;
  3. }

  4. function multiply(a, b) {
  5. return a * b;
  6. }

  7. const sampleArray = [1, 2, 3, 4];

  8. const sum = sampleArray.reduce(add, 0);
  9. console.log(‘The sum total is:’, sum);
  10. // ⦘ The sum total is: 10

  11. const product = sampleArray.reduce(multiply, 1);
  12. console.log(‘The product total is:’, product);
  13. // ⦘ The product total is: 24

我说这个不是针对个人, MDN 文档(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce)也是使用这样的例子。而且我自己也这样使用(https://jrsinclair.com/articles/2016/gentle-introduction-to-functional-javascript-arrays/#reduce)。我们这样做是有原因的。像 ​add()​​ 和 ​multiply()​​ 这样的函数很容易理解。但有点太简单了。对于 ​add()​​ ,是 ​b+a​​ 还是 ​a+b​​ 并不重要,乘法也是一样。​a*b​​ 等于 ​b*a​​。但实际上 ​reducer​ 函数中到底发生了什么。

Reducer​​ 函数是给 ​.reduce()​​ 传递的第一个参数 ​accumulator​。示意如下:

  1. function myReducer(accumulator, arrayElement) {
  2. // Code to do something goes here
  3. }

accumulator​​是一个叠加值。它包含上次调用 ​reducer​​ 函数时返回的所有内容。如果 ​reducer​​ 函数还没有被调用,那么它包含初始值。因此,当我们传递 ​add()​​ 作为 ​reducer​​时,累加器映射到 ​a+b​​ 的 ​a​​ 部分,而 ​a​​ 恰好包含前面所有项目的运行总数。对于 ​multiply()​​也是一样。​a*b​​ 中的 ​a​​ 参数包含运行的乘法总数。这些介绍没什么问题。但是,它掩盖了一个 ​.reduce()​ 最有趣的特征。

reduce()​​有一个强大的能力是 ​accumulator​​ 和 ​arrayElement​ 不必是相同的类型。对于加法和乘法,是同一类型的,a 和 b 都是数字。但其实我们不需要类型相同。累加器可以是与数组元素完全不同的类型。

例如,我们的累加器可能是一个字符串,而我们的数组是数字:

  1. function fizzBuzzReducer(acc, element) {
  2. if (element % 15 === 0) return `${acc}Fizz Buzz
    `;
  3. if (element % 5 === 0) return `${acc}Fizz
    `;
  4. if (element % 3 === 0) return `${acc}Buzz
    `;
  5. return `${acc}${element}
    `;
  6. }

  7. const nums = [
  8. 1, 2, 3, 4, 5, 6, 7, 8, 9,
  9. 10, 11, 12, 13, 14, 15
  10. ];

  11. console.log(nums.reduce(fizzBuzzReducer, ''));

这个例子只是为了举例说明。我们也可以使用 ​.map()​​ , ​.join()​​来实现相同逻辑。​reduce()​​ 不仅仅是对字符串好用。​accumulator​​ 的值可以不是简单的类型(如数字或字符串)。还可以是一个结构化类型,比如数组或者普通的 ​ol'JavaScript​​ 对象( ​POJO​)。接下来,我们做一些更有趣的事情。

我们可以用 reduce 做一些有趣的事情

那么,我们能做些什么有趣的事情呢?我在这里列出了五个不同于数字相加的:

  1. 将数组转换为对象;
  2. 展开成一个更大的阵列;
  3. 在一个遍历中进行两次计算;
  4. 将映射和过滤合并为一个通道;
  5. 按顺序运行异步函数

将数组转换为对象

我们可以使用 ​.reduce()​​ 将数组转换为 ​POJO​。如果您需要进行某种查找,这可能很方便。例如,假如我们有一个人员列表:

  1. const peopleArr = [
  2. {
  3. username: 'glestrade',
  4. displayname: 'Inspector Lestrade',
  5. email: 'glestrade@met.police.uk',
  6. authHash: 'bdbf9920f42242defd9a7f76451f4f1d',
  7. lastSeen: '2019-05-13T11:07:22+00:00',
  8. },
  9. {
  10. username: 'mholmes',
  11. displayname: 'Mycroft Holmes',
  12. email: 'mholmes@gov.uk',
  13. authHash: 'b4d04ad5c4c6483cfea030ff4e7c70bc',
  14. lastSeen: '2019-05-10T11:21:36+00:00',
  15. },
  16. {
  17. username: 'iadler',
  18. displayname: 'Irene Adler',
  19. email: null,
  20. authHash: '319d55944f13760af0a07bf24bd1de28',
  21. lastSeen: '2019-05-17T11:12:12+00:00',
  22. },
  23. ];

在某些情况下,通过用户名查找用户详细信息可能很方便。为了方便起见,我们可以将数组转换为对象。它可能看起来像这样:

  1. function keyByUsernameReducer(acc, person) {
  2. return {...acc, [person.username]: person};
  3. }
  4. const peopleObj = peopleArr.reduce(keyByUsernameReducer, {});
  5. console.log(peopleObj);
  6. // ⦘ {
  7. // "glestrade": {
  8. // "username": "glestrade",
  9. // "displayname": "Inspector Lestrade",
  10. // "email": "glestrade@met.police.uk",
  11. // "authHash": "bdbf9920f42242defd9a7f76451f4f1d",
  12. // "lastSeen": "2019-05-13T11:07:22+00:00"
  13. // },
  14. // "mholmes": {
  15. // "username": "mholmes",
  16. // "displayname": "Mycroft Holmes",
  17. // "email": "mholmes@gov.uk",
  18. // "authHash": "b4d04ad5c4c6483cfea030ff4e7c70bc",
  19. // "lastSeen": "2019-05-10T11:21:36+00:00"
  20. // },
  21. // "iadler":{
  22. // "username": "iadler",
  23. // "displayname": "Irene Adler",
  24. // "email": null,
  25. // "authHash": "319d55944f13760af0a07bf24bd1de28",
  26. // "lastSeen": "2019-05-17T11:12:12+00:00"
  27. // }
  28. // }

在这个版本中,对象中依然包含了用户名。如果你不需要的话,可以移除。

将一个小阵列展开为一个大阵列

通常情况下,我们想到使用 ​.reduce()​​ 就是将许多列表减少到一个值。但是单一值也可以是个数组啊。而且也没有规则说数组必须比原始数组短。所以,我们可以使用 ​.reduce()​将短数组转换为长数组。

假设您从文本文件中读取数据。看下面这个例子。我们在一个数组里放一些纯文本。用逗号分隔每一行,而且假设是一个很大的名字列表。

  1. const fileLines = [
  2. 'Inspector Algar,Inspector Bardle,Mr. Barker,Inspector Barton',
  3. 'Inspector Baynes,Inspector Bradstreet,Inspector Sam Brown',
  4. 'Monsieur Dubugue,Birdy Edwards,Inspector Forbes,Inspector Forrester',
  5. 'Inspector Gregory,Inspector Tobias Gregson,Inspector Hill',
  6. 'Inspector Stanley Hopkins,Inspector Athelney Jones'
  7. ];

  8. function splitLineReducer(acc, line) {
  9. return acc.concat(line.split(/,/g));
  10. }
  11. const investigators = fileLines.reduce(splitLineReducer, []);
  12. console.log(investigators);
  13. // ⦘ [
  14. // "Inspector Algar",
  15. // "Inspector Bardle",
  16. // "Mr. Barker",
  17. // "Inspector Barton",
  18. // "Inspector Baynes",
  19. // "Inspector Bradstreet",
  20. // "Inspector Sam Brown",
  21. // "Monsieur Dubugue",
  22. // "Birdy Edwards",
  23. // "Inspector Forbes",
  24. // "Inspector Forrester",
  25. // "Inspector Gregory",
  26. // "Inspector Tobias Gregson",
  27. // "Inspector Hill",
  28. // "Inspector Stanley Hopkins",
  29. // "Inspector Athelney Jones"
  30. // ]

我们输入一个长度为5的数组,输出了一个长度为16的数组。

现在,你可能以前看过我的 JavaScript 数组方法文明指南(https://jrsinclair.com/javascript-array-methods-cheat-sheet)。那可能会记得我推荐使用 ​.flatMap()​​ 来实现这个功能。但是 ​.flatMap()​​ 在 ​InternetExplorer​​ 或 ​Edge​​ 中是不可用的。所以,我们可以使用 ​.reduce()​​ 来自己实现一个 ​.flatMap()​ 函数。

  1. function flatMap(f, arr) {
  2. const reducer = (acc, item) => acc.concat(f(item));
  3. return arr.reduce(reducer, []);
  4. }

  5. const investigators = flatMap(x => x.split(','), fileLines);
  6. console.log(investigators);

reduce()​ 可以帮助我们把短数组变成长数组。而且它还可以覆盖那些不可用的丢失的数组方法。

在一个遍历中进行两次计算

有时我们需要一个数组进行两次计算。假设,我们希望计算出一个数字列表里的最大值和最小值。我们可能需要这样算两次:

  1. const readings = [0.3, 1.2, 3.4, 0.2, 3.2, 5.5, 0.4];
  2. const maxReading = readings.reduce((x, y) => Math.max(x, y), Number.MIN_VALUE);
  3. const minReading = readings.reduce((x, y) => Math.min(x, y), Number.MAX_VALUE);
  4. console.log({minReading, maxReading});
  5. // ⦘ {minReading: 0.2, maxReading: 5.5}

遍历两次我们的数组。但能不能一次解决呢?​.reduce()​ 可以返回任何我们想要的类型,不必返回一个数字。我们可以将两个值编码到一个对象中。然后我们可以对每次迭代进行两次计算,只遍历一次数组:

  1. const readings = [0.3, 1.2, 3.4, 0.2, 3.2, 5.5, 0.4];
  2. function minMaxReducer(acc, reading) {
  3. return {
  4. minReading: Math.min(acc.minReading, reading),
  5. maxReading: Math.max(acc.maxReading, reading),
  6. };
  7. }
  8. const initMinMax = {
  9. minReading: Number.MAX_VALUE,
  10. maxReading: Number.MIN_VALUE,
  11. };
  12. const minMax = readings.reduce(minMaxReducer, initMinMax);
  13. console.log(minMax);
  14. // ⦘ {minReading: 0.2, maxReading: 5.5}

这个例子里,我们没有考虑到性能。我们仍然需要计算相同的数字。但是在某些情况下,可能会有本质区别。比如,如果我们使用 ​.map()​​ 和 ​.filter()​ 操作...

将 map 和 filter 合成一次传参

假设还是刚刚的那个 ​peopleArr​ 数组。我们排除没有电子邮件地址的人,想找到最近登录的人。一种方法是通过三个独立的操作:

  1. 过滤掉没有电子邮件的人;
  2. 找到最后登录时间
  3. 求最大值

按123写代码如下:

  1. function notEmptyEmail(x) {
  2. return (x.email !== null) && (x.email !== undefined);
  3. }

  4. function getLastSeen(x) {
  5. return x.lastSeen;
  6. }

  7. function greater(a, b) {
  8. return (a > b) ? a : b;
  9. }

  10. const peopleWithEmail = peopleArr.filter(notEmptyEmail);
  11. const lastSeenDates = peopleWithEmail.map(getLastSeen);
  12. const mostRecent = lastSeenDates.reduce(greater, '');

  13. console.log(mostRecent);
  14. // ⦘ 2019-05-13T11:07:22+00:00

这段代码是易读且可执行的。对于样本数据来说,这就足够了。但如果我们有一个巨大的数组,那么我们可能会遇到内存问题。因为我们使用了一个变量来存储每个中间数组。那我们来修改一下我们的 ​reducer​ 方法,一次性完成所有的事情:

  1. function notEmptyEmail(x) {
  2. return (x.email !== null) && (x.email !== undefined);
  3. }

  4. function greater(a, b) {
  5. return (a > b) ? a : b;
  6. }
  7. function notEmptyMostRecent(currentRecent, person) {
  8. return (notEmptyEmail(person))
  9. ? greater(currentRecent, person.lastSeen)
  10. : currentRecent;
  11. }

  12. const mostRecent = peopleArr.reduce(notEmptyMostRecent, '');

  13. console.log(mostRecent);
  14. // ⦘ 2019-05-13T11:07:22+00:00

在这个版本中,我们只需要遍历数组一次。但是,如果人数很少的话,我依然会推荐您使用 ​.filter()​​ 和 ​.map()​。如果您遇到来内存使用或性能问题,再考虑这样的替代方案。

按顺序执行异步函数

我们还可以使用 ​.reduce()​​ 是实现按顺序执行 Promise (与并行相反)。如果对 API 请求有速率限制,或者需要将每个 ​promise​​ 传递给下一个 ​promise​​,用这个方法会很方便。举个例子,假设我们想要获取 ​peopleArr​ 数组中每个人的消息。

  1. function fetchMessages(username) {
  2. return fetch(`https://example.com/api/messages/${username}`)
  3. .then(response => response.json());
  4. }

  5. function getUsername(person) {
  6. return person.username;
  7. }

  8. async function chainedFetchMessages(p, username) {
  9. // In this function, p is a promise. We wait for it to finish,
  10. // then run fetchMessages().
  11. const obj = await p;
  12. const data = await fetchMessages(username);
  13. return { ...obj, [username]: data};
  14. }

  15. const msgObj = peopleArr
  16. .map(getUsername)
  17. .reduce(chainedFetchMessages, Promise.resolve({}))
  18. .then(console.log);
  19. // ⦘ {glestrade: [ … ], mholmes: [ … ], iadler: [ … ]}

请注意,为了使代码正常工作,我们必须传入一个 ​Promise​​ 作为使用 ​Promise.resolve()​​ 的初始值。​resolve​​ 将立即执行(Promise.resolve() 来实现)。然后,我们第一次调用的 ​API​就会立即执行。

为什么我们很少会看到 ​​reduce​​ 的使用呢?

我已经为您展示了各式各样的使用 ​.reduce()​​ 来实现的有趣的事。希望你可以在你的项目中真正的使用起来。不过, ​.reduce()​​ 如此强大和灵活,那么为什么我们很少看到它呢?这是因为,.reduce() 足够灵活和强大,可以做太多事情,进而导致很难具体地、描述它。反而是 ​.map()​​, ​.filter()​​ 和 ​.flatMap()​​ 缺少灵活性,我们会见到更多具体案例场景。还可以看到开发者的意图,让代码可读性更好,所以通常使用其他方法,比 ​reduce​ 的要多。

动手试试吧,我的朋友

现在你对 ​.reduce()​有了改观性的认识,那要不要试试?如果你在尝试过程中发现了我不知道的有趣的事,可以告诉我(https://twitter.com/jrsinclair) 。我很乐意与你交流。

  1. 作者: @js 啦啦队长,2019年5月15日,
  2. (https://twitter.com/JS_Cheerleader/status/1128420687712886784)
  3. 如果你看一下 .reduce()
    (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) 文档,您将看到 ​​reducer​​ 最多需要四个参数。但是只有 ​​accumulator​​ 和 ​​arrayElement​​ 是必传。为了简化,我这里没有传递完整参数。
  4. 一些读者可能会指出,我们可以通过改变 ​accumulator​​ 来获得性能增益。我们可以改变对象,而不是每次都使用 ​​spread​​ 操作符来创建一个新对象。我这样编码是因为我想保持避免操作冲突。但如果会影响性能,那我在实际生产环境代码中,可能会选择改变它。
  5. 如果您想知道如何并行运行 ​Promises​​,请查看如何并行执行
    Promise(https://jrsinclair.com/articles/2019/how-to-run-async-js-in-parallel-or-sequential/)

原文链接:​​ https://jrsinclair.com/articles/2019/functional-js-do-more-with-reduce/​​


【JS】379- 教你玩转数组 reduce_javascript


举报

相关推荐

0 条评论