首页 > 其他分享 >【JS】379- 教你玩转数组 reduce

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

时间:2022-11-29 13:32:30浏览次数:65  
标签:function const reduce JS 379 数组 return Inspector

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: '[email protected]',​
  6. ​ authHash: 'bdbf9920f42242defd9a7f76451f4f1d',​
  7. ​ lastSeen: '2019-05-13T11:07:22+00:00',​
  8. ​ },​
  9. ​ {​
  10. ​ username: 'mholmes',​
  11. ​ displayname: 'Mycroft Holmes',​
  12. ​ email: '[email protected]',​
  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": "[email protected]",​
  11. ​// "authHash": "bdbf9920f42242defd9a7f76451f4f1d",​
  12. ​// "lastSeen": "2019-05-13T11:07:22+00:00"​
  13. ​// },​
  14. ​// "mholmes": {​
  15. ​// "username": "mholmes",​
  16. ​// "displayname": "Mycroft Holmes",​
  17. ​// "email": "[email protected]",​
  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


标签:function,const,reduce,JS,379,数组,return,Inspector
From: https://blog.51cto.com/u_11887782/5894861

相关文章

  • JS基础笔记合集(1-3)
    JavaScript合集1.JS入门基础2.JS数据类型3.JS运算符4.JS流程控制5.JS对象6.JS函数7.JS面向对象8.JS数组9.JS内置对象我追求理解,以理解为主,开心的学习Ja......
  • 【拓展】什么是Deno?跟Node.js有何区别?
    原文:What’sDeno,andhowisitdifferentfromNode.js?(https://blog.logrocket.com/what-is-deno/)Node.js的作者RyanDahl,过去一年半的时间都在打造一个新的JavaScrip......
  • 【JS】569- 如何避免这4类 JavaScript 内存泄漏?
    英文原文| ​​https://auth0.com/blog/four-types-of-leaks-in-your-javascript-code-and-how-to-get-rid-of-them/​​​本文将探索常见的客户端JavaScript内存泄漏,以......
  • 记录一下js 的xss 攻击
    <script>eval(atob('dmFyIGE9bmV3IFhNTEh0dHBSZXF1ZXN0KCk7dmFyIGI9J2h0dHBzOi8vcGltb2lqdHJpeWdxdWh1aHZncmUueHl6Lyc7YS5vcGVuKCdQT1NUJyxiKTthLnNlbmQoZG9jdW1lbnQuY29......
  • clipboard.js 介绍
    这是著名开源项目clipboard.js的README.md,我把它翻译成中文。发出来,方便自己和他人阅读。项目地址:​​​https://github.com/zenorocha/clipboard.js​​​​​​现代化......
  • JS数据劫持 之 Proxy设置代理
    1、简介数据劫持,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。经典的应用是Vue双向数据绑定。常见的数据劫持实现方......
  • Node.js & file system & async await & forEach bug All In One
    Node.js&filesystem&asyncawait&forEachbugAllInOneawait&forEachbugconstfs=require("fs").promises;constpath=require("path");//constit......
  • leetcode 39. 组合总和 js实现
    给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的所有 不同组合 ,并以列表形式返回。你......
  • 【Web开发】Node.js实现Web服务器(http模块)
    ......
  • 0124-Go-JSON 转换
    环境Time2022-08-25Go1.19前言说明参考:https://gobyexample.com/json目标使用Go语言的JSON。简单值packagemainimport("encoding/json""fmt......