【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()
。下面这样的教程并不少见:
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
const sampleArray = [1, 2, 3, 4];
const sum = sampleArray.reduce(add, 0);
console.log(‘The sum total is:’, sum);
// ⦘ The sum total is: 10
const product = sampleArray.reduce(multiply, 1);
console.log(‘The product total is:’, product);
// ⦘ 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
。示意如下:
function myReducer(accumulator, arrayElement) {
// Code to do something goes here
}
accumulator
是一个叠加值。它包含上次调用 reducer
函数时返回的所有内容。如果 reducer
函数还没有被调用,那么它包含初始值。因此,当我们传递 add()
作为 reducer
时,累加器映射到 a+b
的 a
部分,而 a
恰好包含前面所有项目的运行总数。对于 multiply()
也是一样。a*b
中的 a
参数包含运行的乘法总数。这些介绍没什么问题。但是,它掩盖了一个 .reduce()
最有趣的特征。
reduce()
有一个强大的能力是 accumulator
和 arrayElement
不必是相同的类型。对于加法和乘法,是同一类型的,a 和 b 都是数字。但其实我们不需要类型相同。累加器可以是与数组元素完全不同的类型。
例如,我们的累加器可能是一个字符串,而我们的数组是数字:
function fizzBuzzReducer(acc, element) {
if (element % 15 === 0) return `${acc}Fizz Buzz
`;
if (element % 5 === 0) return `${acc}Fizz
`;
if (element % 3 === 0) return `${acc}Buzz
`;
return `${acc}${element}
`;
}
const nums = [
1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15
];
console.log(nums.reduce(fizzBuzzReducer, ''));
这个例子只是为了举例说明。我们也可以使用 .map()
, .join()
来实现相同逻辑。reduce()
不仅仅是对字符串好用。accumulator
的值可以不是简单的类型(如数字或字符串)。还可以是一个结构化类型,比如数组或者普通的 ol'JavaScript
对象( POJO
)。接下来,我们做一些更有趣的事情。
我们可以用 reduce 做一些有趣的事情
那么,我们能做些什么有趣的事情呢?我在这里列出了五个不同于数字相加的:
将数组转换为对象;
展开成一个更大的阵列;
在一个遍历中进行两次计算;
将映射和过滤合并为一个通道;
按顺序运行异步函数
将数组转换为对象
我们可以使用 .reduce()
将数组转换为 POJO
。如果您需要进行某种查找,这可能很方便。例如,假如我们有一个人员列表:
const peopleArr = [
{
username: 'glestrade',
displayname: 'Inspector Lestrade',
email: 'glestrade@met.police.uk',
authHash: 'bdbf9920f42242defd9a7f76451f4f1d',
lastSeen: '2019-05-13T11:07:22+00:00',
},
{
username: 'mholmes',
displayname: 'Mycroft Holmes',
email: 'mholmes@gov.uk',
authHash: 'b4d04ad5c4c6483cfea030ff4e7c70bc',
lastSeen: '2019-05-10T11:21:36+00:00',
},
{
username: 'iadler',
displayname: 'Irene Adler',
email: null,
authHash: '319d55944f13760af0a07bf24bd1de28',
lastSeen: '2019-05-17T11:12:12+00:00',
},
];
在某些情况下,通过用户名查找用户详细信息可能很方便。为了方便起见,我们可以将数组转换为对象。它可能看起来像这样:
function keyByUsernameReducer(acc, person) {
return {...acc, [person.username]: person};
}
const peopleObj = peopleArr.reduce(keyByUsernameReducer, {});
console.log(peopleObj);
// ⦘ {
// "glestrade": {
// "username": "glestrade",
// "displayname": "Inspector Lestrade",
// "email": "glestrade@met.police.uk",
// "authHash": "bdbf9920f42242defd9a7f76451f4f1d",
// "lastSeen": "2019-05-13T11:07:22+00:00"
// },
// "mholmes": {
// "username": "mholmes",
// "displayname": "Mycroft Holmes",
// "email": "mholmes@gov.uk",
// "authHash": "b4d04ad5c4c6483cfea030ff4e7c70bc",
// "lastSeen": "2019-05-10T11:21:36+00:00"
// },
// "iadler":{
// "username": "iadler",
// "displayname": "Irene Adler",
// "email": null,
// "authHash": "319d55944f13760af0a07bf24bd1de28",
// "lastSeen": "2019-05-17T11:12:12+00:00"
// }
// }
在这个版本中,对象中依然包含了用户名。如果你不需要的话,可以移除。
将一个小阵列展开为一个大阵列
通常情况下,我们想到使用 .reduce()
就是将许多列表减少到一个值。但是单一值也可以是个数组啊。而且也没有规则说数组必须比原始数组短。所以,我们可以使用 .reduce()
将短数组转换为长数组。
假设您从文本文件中读取数据。看下面这个例子。我们在一个数组里放一些纯文本。用逗号分隔每一行,而且假设是一个很大的名字列表。
const fileLines = [
'Inspector Algar,Inspector Bardle,Mr. Barker,Inspector Barton',
'Inspector Baynes,Inspector Bradstreet,Inspector Sam Brown',
'Monsieur Dubugue,Birdy Edwards,Inspector Forbes,Inspector Forrester',
'Inspector Gregory,Inspector Tobias Gregson,Inspector Hill',
'Inspector Stanley Hopkins,Inspector Athelney Jones'
];
function splitLineReducer(acc, line) {
return acc.concat(line.split(/,/g));
}
const investigators = fileLines.reduce(splitLineReducer, []);
console.log(investigators);
// ⦘ [
// "Inspector Algar",
// "Inspector Bardle",
// "Mr. Barker",
// "Inspector Barton",
// "Inspector Baynes",
// "Inspector Bradstreet",
// "Inspector Sam Brown",
// "Monsieur Dubugue",
// "Birdy Edwards",
// "Inspector Forbes",
// "Inspector Forrester",
// "Inspector Gregory",
// "Inspector Tobias Gregson",
// "Inspector Hill",
// "Inspector Stanley Hopkins",
// "Inspector Athelney Jones"
// ]
我们输入一个长度为5的数组,输出了一个长度为16的数组。
现在,你可能以前看过我的 JavaScript 数组方法文明指南(https://jrsinclair.com/javascript-array-methods-cheat-sheet)。那可能会记得我推荐使用 .flatMap()
来实现这个功能。但是 .flatMap()
在 InternetExplorer
或 Edge
中是不可用的。所以,我们可以使用 .reduce()
来自己实现一个 .flatMap()
函数。
function flatMap(f, arr) {
const reducer = (acc, item) => acc.concat(f(item));
return arr.reduce(reducer, []);
}
const investigators = flatMap(x => x.split(','), fileLines);
console.log(investigators);
reduce()
可以帮助我们把短数组变成长数组。而且它还可以覆盖那些不可用的丢失的数组方法。
在一个遍历中进行两次计算
有时我们需要一个数组进行两次计算。假设,我们希望计算出一个数字列表里的最大值和最小值。我们可能需要这样算两次:
const readings = [0.3, 1.2, 3.4, 0.2, 3.2, 5.5, 0.4];
const maxReading = readings.reduce((x, y) => Math.max(x, y), Number.MIN_VALUE);
const minReading = readings.reduce((x, y) => Math.min(x, y), Number.MAX_VALUE);
console.log({minReading, maxReading});
// ⦘ {minReading: 0.2, maxReading: 5.5}
遍历两次我们的数组。但能不能一次解决呢?.reduce()
可以返回任何我们想要的类型,不必返回一个数字。我们可以将两个值编码到一个对象中。然后我们可以对每次迭代进行两次计算,只遍历一次数组:
const readings = [0.3, 1.2, 3.4, 0.2, 3.2, 5.5, 0.4];
function minMaxReducer(acc, reading) {
return {
minReading: Math.min(acc.minReading, reading),
maxReading: Math.max(acc.maxReading, reading),
};
}
const initMinMax = {
minReading: Number.MAX_VALUE,
maxReading: Number.MIN_VALUE,
};
const minMax = readings.reduce(minMaxReducer, initMinMax);
console.log(minMax);
// ⦘ {minReading: 0.2, maxReading: 5.5}
这个例子里,我们没有考虑到性能。我们仍然需要计算相同的数字。但是在某些情况下,可能会有本质区别。比如,如果我们使用 .map()
和 .filter()
操作...
将 map 和 filter 合成一次传参
假设还是刚刚的那个 peopleArr
数组。我们排除没有电子邮件地址的人,想找到最近登录的人。一种方法是通过三个独立的操作:
过滤掉没有电子邮件的人;
找到最后登录时间
求最大值
按123写代码如下:
function notEmptyEmail(x) {
return (x.email !== null) && (x.email !== undefined);
}
function getLastSeen(x) {
return x.lastSeen;
}
function greater(a, b) {
return (a > b) ? a : b;
}
const peopleWithEmail = peopleArr.filter(notEmptyEmail);
const lastSeenDates = peopleWithEmail.map(getLastSeen);
const mostRecent = lastSeenDates.reduce(greater, '');
console.log(mostRecent);
// ⦘ 2019-05-13T11:07:22+00:00
这段代码是易读且可执行的。对于样本数据来说,这就足够了。但如果我们有一个巨大的数组,那么我们可能会遇到内存问题。因为我们使用了一个变量来存储每个中间数组。那我们来修改一下我们的 reducer
方法,一次性完成所有的事情:
function notEmptyEmail(x) {
return (x.email !== null) && (x.email !== undefined);
}
function greater(a, b) {
return (a > b) ? a : b;
}
function notEmptyMostRecent(currentRecent, person) {
return (notEmptyEmail(person))
? greater(currentRecent, person.lastSeen)
: currentRecent;
}
const mostRecent = peopleArr.reduce(notEmptyMostRecent, '');
console.log(mostRecent);
// ⦘ 2019-05-13T11:07:22+00:00
在这个版本中,我们只需要遍历数组一次。但是,如果人数很少的话,我依然会推荐您使用 .filter()
和 .map()
。如果您遇到来内存使用或性能问题,再考虑这样的替代方案。
按顺序执行异步函数
我们还可以使用 .reduce()
是实现按顺序执行 Promise (与并行相反)。如果对 API 请求有速率限制,或者需要将每个 promise
传递给下一个 promise
,用这个方法会很方便。举个例子,假设我们想要获取 peopleArr
数组中每个人的消息。
function fetchMessages(username) {
return fetch(`https://example.com/api/messages/${username}`)
.then(response => response.json());
}
function getUsername(person) {
return person.username;
}
async function chainedFetchMessages(p, username) {
// In this function, p is a promise. We wait for it to finish,
// then run fetchMessages().
const obj = await p;
const data = await fetchMessages(username);
return { ...obj, [username]: data};
}
const msgObj = peopleArr
.map(getUsername)
.reduce(chainedFetchMessages, Promise.resolve({}))
.then(console.log);
// ⦘ {glestrade: [ … ], mholmes: [ … ], iadler: [ … ]}
请注意,为了使代码正常工作,我们必须传入一个 Promise
作为使用 Promise.resolve()
的初始值。resolve
将立即执行(Promise.resolve() 来实现)。然后,我们第一次调用的 API
就会立即执行。
为什么我们很少会看到 reduce
的使用呢?
我已经为您展示了各式各样的使用 .reduce()
来实现的有趣的事。希望你可以在你的项目中真正的使用起来。不过, .reduce()
如此强大和灵活,那么为什么我们很少看到它呢?这是因为,.reduce() 足够灵活和强大,可以做太多事情,进而导致很难具体地、描述它。反而是 .map()
, .filter()
和 .flatMap()
缺少灵活性,我们会见到更多具体案例场景。还可以看到开发者的意图,让代码可读性更好,所以通常使用其他方法,比 reduce
的要多。
动手试试吧,我的朋友
现在你对 .reduce()
有了改观性的认识,那要不要试试?如果你在尝试过程中发现了我不知道的有趣的事,可以告诉我(https://twitter.com/jrsinclair) 。我很乐意与你交流。
作者: @js 啦啦队长,2019年5月15日,
(https://twitter.com/JS_Cheerleader/status/1128420687712886784)
如果你看一下 .reduce()
(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) 文档,您将看到 reducer
最多需要四个参数。但是只有 accumulator
和 arrayElement
是必传。为了简化,我这里没有传递完整参数。
一些读者可能会指出,我们可以通过改变 accumulator
来获得性能增益。我们可以改变对象,而不是每次都使用 spread
操作符来创建一个新对象。我这样编码是因为我想保持避免操作冲突。但如果会影响性能,那我在实际生产环境代码中,可能会选择改变它。
如果您想知道如何并行运行 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的更多相关文章
- JS进阶篇--JS数组reduce()方法详解及高级技巧
基本概念 reduce() 方法接收一个函数作为累加器(accumulator),数组中的每个值(从左到右)开始缩减,最终为一个值. reduce 为数组中的每一个元素依次执行回调函数,不包括数组中被 ...
- 手把手教你玩转SOCKET模型之重叠I/O篇(下)
四. 实现重叠模型的步骤 作 了这么多的准备工作,费了这么多的笔墨,我们终于可以开始着手编码了.其实慢慢的你就会明白,要想透析重叠结构的内部原理也许是要费点功夫,但是只是学会 如何来使用它,却 ...
- 转:变手把手教你玩转SOCKET模型之重叠I/O篇
手把手教你玩转SOCKET模型之重叠I/O篇 “身为一个初学者,时常能体味到初学者入门的艰辛,所以总是想抽空作点什么来尽我所能的帮助那些需要帮助的人.我也希望大家能把自己的所学和他人一起分享,不要去鄙 ...
- Js中常用的字符串,数组,函数扩展
由于最近辞职在家,自己的时间相对多一点.所以就根据prototytpeJS的API,结合自己正在看的司徒大神的<javascript框架设计>,整理了下Js中常用一些字符串,数组,函数扩展 ...
- JS分割字符串并放入数组的函数
JS分割字符串并放入数组的函数: var InterestKeywordListString = $("#userInterestKeywordLabel").html(); v ...
- 教你使用shell数组
数组的使用,需要掌握 1.对数组进行赋值 2.通过下标访问数组元素 3.循环遍历所有的元素 代码如下: #!/bin/bash a="39" b="5" c=& ...
- JS中几种常见的数组算法(前端面试必看)
JS中几种常见的数组算法 1.将稀疏数组变成不稀疏数组 /** * 稀疏数组 变为 不稀疏数组 * @params array arr 稀疏数组 * @return array 不稀疏的数组 */ f ...
- php中向前台js中传送一个二维数组
在php中向前台js中传送一个二维数组,并在前台js接收获取其中值的全过程方法: (1),方法说明:现在后台将数组发送到前台 echo json_encode($result); 然后再在js页面中的 ...
- 腾讯工程师教你玩转 RocksDB
欢迎大家前往云+社区,获取更多腾讯海量技术实践干货哦~ 作者:腾讯云数据库内核团队 原文标题:[腾讯云CDB]教你玩转MyRocks/RocksDB-STATISTICS与后台线程篇 0. Intro ...
随机推荐
- [笔记]IDEA使用笔记
1.IDEA的目录结构 2.所有的源文件都必须写在src文件夹下, 3.输入psvm再按回车,就会生成主函数: 4.输入sout就会生成输出语句的格式: 5.ALT+4 调出上次运行的结果出来看看 ...
- Graphviz 画图的一些总结
Graphviz Graphviz 是一个自动排版的作图软件,可以生成 png pdf 等格式. 一切以官方文档为准,博客只是参考.这里做一个自己学习的记录. dot 语法介绍 部分图形属性介绍 示例 ...
- TreeMap树映射取出对象的方式
1.直接获取该TreeMap集合中的关系:entrySet() Map接口中的方法,返回值类型是该集合中的各个关系:返回值类型是:Set类型的Map.EntrySet类型:然后在通过Set集合中特有的 ...
- three.js使用gpu选取物体并计算交点位置
光线投射法 使用three.js自带的光线投射器(Raycaster)选取物体非常简单,代码如下所示: var raycaster = new THREE.Raycaster(); var mouse ...
- Unittest框架的从零到壹(一)
前言 Python中有非常多的单元测试框架,如unittest.pytest.nose.doctest等,Python2.1及其以后的版本已经将unittest作为一个标准模块放入Python开发包中 ...
- 【Luogu P1439】最长公共子序列(LCS)
Luogu P1439 令f[i][j]表示a的前i个元素与b的前j个元素的最长公共子序列 可以得到状态转移方程: if (a[i]==b[j]) dp[i][j]=dp[i-1][j-1]+1; d ...
- 【集训Day1 测试】奇怪数
奇怪数(odometer) [题目描述] 一个正整数Z是奇怪数,当且仅当满足的条件是:Z的所有数字中,只有一个数字不同于其他数字.例如:33323.110 都是奇怪数,而 9779.5555 都不是奇 ...
- Java 虚拟机结构
一 数据类型 与 Java 程序语言中的数据类型相似,Java 虚拟机可以操作的数据类型可分为两类:原始类型(Primitive Types,也经常翻译为原生类型或者基本类型)和引用类型(Refere ...
- CentOS 7 Cobbler 配置 YUM仓库
通过Cobbler配置内网YUM仓库 在上一篇Cobbler 安装中,配置好了Cobbler 下面来通过Cobbler来配置内网的YUM仓库 这里可以同步所有版本的yum源,增加内网的yum安装下载速 ...
- jenkins 如何让job对应一个节点
1.配置job:如图,在label expression 里面填写[节点标签名]或者是[节点名称]. 2.配置节点: 3.构建:第一个红线,表明使用哪个节点进行构建. 第二个红线,表明工作目录.