FRONTEND 十一月 02, 2019

JavaScript中更简便的数组处理函数.map(),.reduce(),.filter()

文章字数 33k 阅读约需 30 mins. 阅读次数 0

如果你刚接触 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循环吧!我保证你的代码会越来越简洁,可读性更高。

如果你喜欢我的这遍文章,记得继续关注我的博客,下一遍文章我们开学习怎么在 JavaScript 中使用.some().find()

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


上一篇:
下一篇:
0%