如果你刚接触 JavaScript,可能你还没有听说过.map()
,.reduce()
,.filter()
。或者你听说过,看过别人用过,但是自己在实际项目中没有用过。国内很多开发项目都是需要考虑 IE8 的兼容,为了兼容很多 JavaScript 好用的方法和技巧都被埋没了。
但是,我发现近几年很多开发项目已经完全抛弃了 IE 这个魔鬼了。如果你不需要兼容“石器时代”的 IE 浏览器了,那就要开始熟悉一下这几个方法来处理数组。
注意这遍文章说的的 3 个方法其实在很多其他语言都可以使用到,因为这几个方法和使用概念在很多其他语言都是存在的。
.map()
让我用一个简单的例子告诉你如何使用这个方法。假如你现在有多对象的数组数据 - 每一个对象代表着一个员工的信息。现在你想要的最终结果就是取出所有员工的唯一 ID 值。
// 员工数据var employees = [ { id: 20, name: 'Captain Piett' }, { id: 24, name: 'General Veers' }, { id: 56, name: 'Admiral Ozzel' }, { id: 88, name: 'Commander Jerjerrod' }];// 你想要的结果[20, 24, 56, 88]
其实要实现这个结果有很多数组处理方式。传统的处理方法就是先定义一个空数组,然后使用.forEach()
,.for(...of)
,或者是最简单的.for()
来组装 ID 到你定义的数组里面。
我们来对比一下传统的处理方式和.map()
的区别。
使用.forEach()
:
var employeeIds = [];employees.forEach(function (employee) { employeeIds.push(officer.id);});
注意,使用传统的方式,我们必须有一个预定义的空数组变量才行,但是如果是.map()
就会更简单了。
var employeeIds = employees.map(function (employee) { return employee.id});
甚至我们可以用更简洁的方式,使用箭头方法(但是需要 ES6 支持,Babel,或者 TypeScript)。
const employeeIds = employees.map(employee => employee.id);
所以.map()
到底是怎么运作的呢?这个方法有两个参数,第一是回调方法,第二是可选内容(会在回调方法中做为this
)。数组里的每个数值/对象会被循环进入到回调方法
里面,然后返回新的数值/对象
到结果数组里面。
注意,结果数组的长度永远都会和被循环的数组的长度一致。
.reduce()
与.map()
相似,.reduce()
也是循环一个回调方法,数组里面的每一个元素都会进入回调方法,区别是回调方法返回的值会被传递到下一个回调方法,如此类推(等同于一个累加器)。
.reduce()
里的累加值可以是任何属性的值,包括integer
,string
,object
等等。这个累加值会被实例化或者传递到下一个回调方法。
来,上代码,做个简单的例子!假如你有一个飞机师的数组,数组里面有每个飞机师的工龄。
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
方法会返回最终累加后的值(在我们这个例子中就是82
)。
例子里面的
acc
和accumulator
就是累加值变量。
如果是使用 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;}, {});
这里我把accumulator
变量改为oldest
,代表飞机师里面的老司机。这时候reduce
里面的回调方法对比每一个飞机师,每一次飞机师的值进入这个回调方法,工龄更高的就会覆盖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", }];
假如现在我们想分别筛选出Rebels
和Empire
两个航空公司的飞机师,使用.filter()
就是轻而易举的事情!
var rebels = pilots.filter(function (pilot) { return pilot.faction === "Rebels";});var empire = pilots.filter(function (pilot) { return pilot.faction === "Empire";});
就这么简单,如果使用箭头方法(ES6)就更加优雅了:
const rebels = pilots.filter(pilot => pilot.faction === "Rebels");const empire = pilots.filter(pilot => pilot.faction === "Empire");
其实原理很简单,只要你的回调方法返回的是true
,这个值或者对象就会在新的数组里面了。如果返回的是false
就会被过滤掉了。
结合使用 .map(),.reduce(),.filter()
既然我们刚刚学到的三个函数都是可以用于数组的,并且.map()
和.filter()
都是返回数组的,那我们可以把它们串联起来使用。不说多了,上代码试试!
我们用一个有趣一点的数据试验一下。假如现在我们有一个星球大战
里面的人物
的数组,每个字段的定义如下:
Id
: 人物唯一ID
name
: 人物名字
pilotingScore
: 飞行能力指数
shootingScore
: 射击能力指数
isForceUser
: 是否拥有隔空操控能力
我们的目标:获取拥有隔空操控能力的飞行员的总飞行能力指数
。我们分开一步一步实现这个目标!
首先我们需要先获取到拥有隔空操控能力的飞行员。
var jediPersonnel = personnel.filter(function (person) { return person.isForceUser;});// 结果集: [{...}, {...}, {...}] (Luke, Ezra and Caleb)
通过这段代码,我们获得了 3 个飞行员对象,分别都是拥有隔空操控能力的飞行员。使用这个对象,我们来获取每个飞行员的飞行能力指数值。
var jediScores = jediPersonnel.map(function (jedi) { return jedi.pilotingScore + jedi.shootingScore;});// 结果: [154, 110, 156]
获取到每个飞行员的飞行能力指数值后,我们就可以用累加器(.reduce()
)来获取总飞行能力指数了。
var totalJediScore = jediScores.reduce(function (acc, score) { return acc + score;}, 0);// 结果: 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);
这样写是不是很优雅!都被这段代码给美到了!❤️
如果我们使用箭头写法 ES6,就更加优雅了!
const totalJediScore = personnel .filter(person => person.isForceUser) .map(jedi => jedi.pilotingScore + jedi.shootingScore) .reduce((acc, score) => acc + score, 0);
哇!代码原来可以写的那么优雅的么?!想不到吧?
其实我们只需要使用
.reduce()
就可以得到我们的目标结果了,以上例子做为教学例子,所以使用了 3 个我们学到的函数。我们来看看只用
.reduce()
怎么实现的。
const totalJediScore = personnel.reduce((acc, person) => person.isForceUser ? acc + person.pilotingScore + person.shootingScore : acc, 0);
不敢想象吧?一行就搞定一个功能不是梦!
为什么抛弃 .forEach()?
其实我一开始写前端的时候也是一顿撸,来个数组都是撸个 for 循环,解决一切数组处理问题。但是近几年我开始步入前后端开发,API 接口对接。发现数据处理越来越多,如果还是像以前那样什么都用 for 循环来处理数据,那其实数据处理的代码就会越来越臃肿越来越复杂凌乱。所以我开始抛弃了.forEach()
,开始做一个优雅的程序员!
为什么使用.map()
,.filter()
,.reduce()
写代码更优雅、更美观呢?我们用一个实战例子来对比一下吧。
假设现在我们对接一个接口,返回的数组里面有两个字段name:人的名称
和title:对应的职位
。
var data = [ { name: "Jan Dodonna", title: "General", }, { name: "Gial Ackbar", title: "Admiral", },]
产品经理给到你的需求是只需要展示这些人的职位称呼。
当然这个时候有一些前端开发者就会说“我只是个小小的前端,让后端给我处理吧”。
但是,这个接口其实是一个通用的接口,就是获取这些员工的资料的,是在多个地方使用的。如果每一个页面因为需要展示的不一样而要写多一个接口给你,你觉得这样好吗?
作为一个优秀的前端工程师🦁️,这种小 case 你自己就可以很优雅地处理好了。而且,在一个优秀的团队里,后端确实是要考虑接口通用性的,这种为了你自己的方便而给他们带来更臃肿的接口是不可接受的。所以前端这个时候就是要重组数据了。
假设现在产品给你的需求是员工列表中要支持只展示”员工职称“和”员工信息“的两种显示项,这个时候我们就要编写一个数据组装方法来跟进展示要求,来改变数据格式。
因为这个“骚“需求,我们使用.forEach()
来重组数据就相对比较麻烦了,而且代码也会变得臃肿。
我们忽略了组装数据的方法,直接就当作我们已经写好了一个组装数据的方法为formatElement
。如果我们用forEach
,那首先就需要定义一个空数组来接收结果。
var results = [];data.forEach(function (element) { var formatted = formatElement(element); results.push(formatted);});
所以我们需要两个方法才能实现这个数据结果,但是为什么要写得那么臃肿呢?因为forEach
并没有返回值,单单就给你跑个循环,还需要自己push
值到预定义的变量里面。其实一个方法就可以完成了,而且重点是一行代码就完事了。
来使用我们新学的技巧,用.map()
来实现就非常简单优雅了。
var results = data.map(formatElement);
总结
你学会了吗?学会了就去尝试用.map()
,.reduce()
,.filter()
来替换你传统的for
循环吧!我保证你的代码会越来越简洁,可读性更高。
坚持做一个优雅的程序员,坚持每天敲代码!