如果你刚接触 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()里的累加值可以是任何属性的值,包括integerstringobject等等。这个累加值会被实例化或者传递到下一个回调方法。

来,上代码,做个简单的例子!假如你有一个飞机师的数组,数组里面有每个飞机师的工龄。

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)。

例子里面的accaccumulator就是累加值变量。

如果是使用 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",  }];

假如现在我们想分别筛选出RebelsEmpire两个航空公司的飞机师,使用.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循环吧!我保证你的代码会越来越简洁,可读性更高。

坚持做一个优雅的程序员,坚持每天敲代码!