假如你刚入坑JavaScript,可能对 .map() 、 .reduce() 和、 .filter() 。对我来说得花一阵子去了解,因为前不久我还得维护IE8。但是,如果不需要兼容低非常古老的浏览器,就需要熟悉这些方法。
请注意本文也可能适用于其他编程语言,因为在其他编程语言中有这些概念。
.map()
举个简单的例子。假设你得到一个包含多个对象的数组——每个对象代表一个人。最后你需要得到一个包含每个人id的数组。
// What you havevar officers = [{ id: 20, name: 'Captain Piett' },{ id: 24, name: 'General Veers' },{ id: 56, name: 'Admiral Ozzel' },{ id: 88, name: 'Commander Jerjerrod' }];// What you need[20, 24, 56, 88]
有很多方法可以实现。你也许想通过创建一个空数组,然后使用 .forEach() , .for(...of) 或者简单的 .for() 来实现。
让我们来对比一下!
使用.forEach() :
var officersIds = [];officers.forEach(function (officer) {officersIds.push(officer.id);});
什么,你需要预先创建一个空数组?让我们看看使用.map() 会是怎么样的:
var officersIds = officers.map(function (officer) {return officer.id});
我们甚至使用箭头函数变得更加简洁(需要ES6支持,Babel或者Typescript)
const officersIds = officers.map(officer => officer.id);
.map() 是怎么工作的?通常情况它接受两个参数,一个callback,和一个可选上下文(执行callback被用作this),这个在上述示例中没有使用。数组中每个元素都会被callback函数调用一次,并且回调函数的结果组成了新数组的每一个元素。
需要注意的是结果数组总是和原数组保持相同的长度。
.reduce()
类似 .map() , .reduce() 也是对数组每个元素执行一个回调函数。不同的是,reduce将callback(累加器)的返回值从一个数组元素传递到另一个数组元素。这个累加器几乎可以是任何类型(整数、字符串、对象等等),并且在调用 .reduce() 必须实例化或传递。
举个例子!假设你有一个数组,它包含一些飞行员及其工龄:
var pilots = [{id: 10,name: "Poe Dameron",years: 14,},{id: 2,name: "Temmin 'Snap' Wexley",years: 30,},{id: 41,name: "Tallissan Lintra",years: 16,},{id: 99,name: "Ello Asty",years: 22,}];
我们需要了解他们工龄总和。直接用 .reduce() :
var totalYears = pilots.reduce(function (accumulator, pilot) {return accumulator + pilot.years;}, 0);
请注意我设置初始值是0。如果需要的话,也可使用一个现有变量作为初始值。reduce在执行callback回调之后将返回累加器最终的值(在例子:82)。
让我们瞧瞧在ES6箭头函数下代码变得更短小精悍:
const totalYears = pilots.reduce((acc, pilot) => acc + pilot.years, 0);
现在假设我想知道哪个飞行员的工龄最大。我也可以像这样使用reduce函数:
var mostExpPilot = pilots.reduce(function (oldest, pilot) {return (oldest.years || 0) > pilot.years ? oldest : pilot;}, {});
我将累加器命名为 oldest 。回调累加器将与每个飞行员做比较。如果某个飞行员的工龄大于累加器,那么这个飞行员的工龄就成为新的 oldest 。
正如你所见,使用.reduce() 从一个数组中生成单个值或者是对象是一件非常容易的事儿。
.filter()
假如有这么一个数组,但仅仅想知道其中是否存在某个元素怎么办?那就是.filter() 登场了!
我们的数组在这儿:
var pilots = [{id: 2,name: "Wedge Antilles",faction: "Rebels",},{id: 8,name: "Ciena Ree",faction: "Empire",},{id: 40,name: "Iden Versio",faction: "Empire",},{id: 66,name: "Thane Kyrell",faction: "Rebels",}];
假设我们想要两个数组:一个是反叛派系(rebel)飞行员,另一个是帝国派系(empire)飞行员。使用.filter() 简直不要太简单了:
var rebels = pilots.filter(function (pilot) {return pilot.faction === "Rebels";});var empire = pilots.filter(function (pilot) {return pilot.faction === "Empire";});
是的!箭头函数甚至更短:
const rebels = pilots.filter(pilot => pilot.faction === "Rebels");const empire = pilots.filter(pilot => pilot.faction === "Empire");
基本上,如果回调函数返回 true,当前元素就出现在结果数组中。相反如果返回false,则不会。
Combining .map(), .reduce(), and .filter()
由于三个方法都是在数组上调用的,并且.map()和.filter() 都返回数组,我们可以轻松地使用链式调用。
来看看另一个例子。这是数据:
var personnel = [{id: 5,name: "Luke Skywalker",pilotingScore: 98,shootingScore: 56,isForceUser: true,},{id: 82,name: "Sabine Wren",pilotingScore: 73,shootingScore: 99,isForceUser: false,},{id: 22,name: "Zeb Orellios",pilotingScore: 20,shootingScore: 59,isForceUser: false,},{id: 15,name: "Ezra Bridger",pilotingScore: 43,shootingScore: 67,isForceUser: true,},{id: 11,name: "Caleb Dume",pilotingScore: 71,shootingScore: 85,isForceUser: true,},];
我们的目标:获取原力用户的总分。让我们一步一步来!
首先,我们需要筛选出不能使用原力(force)的用户:
var jediPersonnel = personnel.filter(function (person) {return person.isForceUser;});// Result: [{...}, {...}, {...}] (Luke, Ezra and Caleb)s
这样我们结果数组中剩下3个元素。我们需要生成一个包含每个绝地武士(Jedi)总分的数组.
var jediScores = jediPersonnel.map(function (jedi) {return jedi.pilotingScore + jedi.shootingScore;});// Result: [154, 110, 156]
然后我们使用 reduce 来获得总分:
var totalJediScore = jediScores.reduce(function (acc, score) {return acc + score;}, 0);// Result: 420
重点来了…我们可以将所有这些调用链接在一起,从而在一行中得到我们想要的东西:
var totalJediScore = personnel.filter(function (person) {return person.isForceUser;}).map(function (jedi) {return jedi.pilotingScore + jedi.shootingScore;}).reduce(function (acc, score) {return acc + score;}, 0);
用了箭头函数之后更棒了:
const totalJediScore = personnel.filter(person => person.isForceUser).map(jedi => jedi.pilotingScore + jedi.shootingScore).reduce((acc, score) => acc + score, 0);
Boom! 💥
注意:在上个例子中,.map()和.filter()并不是必需的。我们可以仅用.reduce()达到相同的结果输出。我在例子里留了思考。你能猜想到怎样才能仅用.reduce()一行代码达到相同的结果吗?See the solution on CodePen
Why not use .forEach()?
I used to use for loops everywhere instead of .map(), .reduce(), and .filter(). But a couple of years ago I started working a lot more with data that came from an API. That’s where I began to see the advantages of leaving .forEach behind.
我曾常常使用for循环而不是 .map() 、 .reduce() 和 .filter() 。但几年前我开始频繁处理从API获取的数据。我就开始不怎么用 .forEach 。
Formatting
假设你需要显示人员列表,包括其姓名和职务。
var data = [{name: "Jan Dodonna",title: "General",},{name: "Gial Ackbar",title: "Admiral",},]
以上数据是API给你的,但你仅仅需要每个人的标题title和姓氏last name…你需要格式化数据。然而,你的应用也需要为每个人展示单视图,所以必须写一个列表视图和单视图两不误的数据格式化函数。
那意味着在格式化函数中不能使用 .forEach 循环,否则,你必须将单个元素包装在数组中,然后才能将其传递给函数,以使其正常工作,像这样:
var result = formatElement([element])[0];// Yeah... that's not right at all
因此循环必须对该函数的调用进行包装,如下所示:
data.forEach(function (element) {var formatted = formatElement(element);// But what then....});
单 .forEach() 不会返回任何元素。它做不到。这意味着你不得不把结果放入一个预数组中。
var results = [];data.forEach(function (element) {var formatted = formatElement(element);results.push(formatted);});
结果,你有2个函数: formatElement() 函数和push结果到数组中的函数。
何必多此一举呢?
var results = data.map(formatElement);
Testing is easier
If you write unit tests for your code, you’ll find it simpler to test the functions you call with .map(),
如果你为代码配置了单元测试,你将发现对使用 .map() 、 .reduce() 或者 .filter() 的函数进行测试变得更简单了。
你需要做的就是给函数提供数据和等结果出来。基本上是“如果通过了会怎样?”。更少操作,更少 beforeEach 和 afterEach 了。简洁明了的测试。
Try it!
在合适的地方试着用.map() 、 .reduce() 或者 .filter() 的代替 for 循环。我保证你的代码看起更加清爽和可读性更佳。
如果你喜欢本文以及想学习更多数组方法,查阅我的文章how to use [.some()](https://medium.com/poka-techblog/simplify-your-javascript-use-some-and-find-f9fb9826ddfd) and [.find()](https://medium.com/poka-techblog/simplify-your-javascript-use-some-and-find-f9fb9826ddfd) in JavaScript.
Keep coding!
原文地址:https://medium.com/poka-techblog/simplify-your-javascript-use-map-reduce-and-filter-bd02c593cc2d 原文作者:Etienne Talbot
