ccforward / cc

Code & Blog
1.59k stars 193 forks source link

55.[函数式编程] 不用循环的 JavaScript #59

Open ccforward opened 7 years ago

ccforward commented 7 years ago

不用循环的 JavaScript -- 函数式编程

通过代码一步步的深入 map reduce filter find ...

参考自 http://jrsinclair.com/articles/2017/javascript-without-loops/

本文重写了代码、添加了个人的理解,更加精简

循环 Loop

先看看js中的基础循环代码 while

// 所有字母大写
function capitalize(str){
  return str.replace(/[a-z]/g, s => s.toUpperCase())
}

const company = [
  'apple',
  'google',
  'amazon',
  'facebook'
]

使用 while 循环来大写数组中每个元素

let i = 0
const len = company.length
const newCompany = []
while(i<len) {
  newCompany.push(capitalize(company[i]))
  i++
}

这种循环太常见了,一个初始值为0计数器 i ,每次循环需要递增 i 的值,并通过 len 来判断循环终止条件。
简单的 for 循环代码如下:

let i = 0
const len = company.length
const newCompany = []
for(; i<len; i++) {
  newCompany.push(capitalize(company[i]))
}

比起 while for 循环把计数器 i 放在循环顶部,避免了 while 中忘记计数器自增自增而引起的死循环。

但是后退一步看,我们只是要把每一项大写,但是并不在乎循环中的计数器 i

因为这种对数组每一项进行循环操作的模式很常见,所以有了 ES2015 中的 for..of 循环

const newCompany = []
for(let item of company) {
  newCompany.push(capitalize(item))
}

用了 for...of 省了计数器和循环终止的判断,也不需要抽取出数组每一项来处理

映射 Mapping

虽然 for...of 更简洁,但还是要初始 newCompany 并执行 push

如何做到更加精简

如果现在有两个数组需要 capitalize

const company = [
  'apple',
  'google',
  'amazon',
  'facebook'
]

const animal = [
  'dog',
  'cat',
  'pig'
]

两次循环 简单粗暴:

const newCompany = []
for(let item of company) {
  newCompany.push(capitalize(item))
}
const newAnimal = []
for(let item of animal) {
  newAnimal.push(capitalize(item))
}

这看起来不是很 DRY (Don't repeat yourself) ,所以可以重构下

function capitalizeArray(arr){
  const newArr = []
  for(let item of arr){
    newArr.push(capitalize(item))
  }

  return newArr
}
const newCompany = capitalizeArray(company)
const newAnimal = capitalizeArray(animal)

这样看起来还行,但是如果新加一个功能,只是首字母大写呢

// 首字母大写
function capitalizeFirst(str){
  return str.replace(/( |^)[a-z]/, s => s.toUpperCase())
}

如果这样写,下面的代码依然不是很 DRY

function capitalizeArray(arr){
  const newArr = []
  for(let item of arr){
    newArr.push(capitalize(item))
  }

  return newArr
}

function capitalizeFirstArray(arr){
  const newArr = []
  for(let item of arr){
    newArr.push(capitalizeFirst(item))
  }

  return newArr
}

那就继续重构,抽象成给定一个数组和一个函数,然后将数组中每一项映射到新的数组的模式

function map(fn, arr) {
  const newArr = []
  for(let item of arr){
    newArr.push(fn(item))
  }

  return newArr
}

但代码里依然有个循环,只能写成递归的形式

function map(fn, arr) {
  if(arr.length == 0) return []
  return fn(arr[0]).concat(map(fn, arr.slice(1)))
}

递归的代码极简,但在旧浏览器里有性能问题,所以我们就直接用 js 内置的 map 好了

同样简单粗暴

const newCompany        = company.map(capitalize)
const firstUpperCompany = company.map(capitalizeFirst)
const newAnimal         = animal.map(capitalize)
const firstUpperAnimal  = animal.map(capitalizeFirst)

像函数式编程一样,分而治之,两个用来做大写字符串的函数只关注本身的功能,不关心数据从哪来;同时 map 只负责传递函数,不关心函数是干什么的。

Reduce

map 在输出和输入的相同长度的数组上很好用。

但是如果再添加一个数字数组或者在一个 list 中找出最短的字符串呢。

如下,有个 worker 的数组,包含姓名和薪水

const workers = [
  { name: 'Jack', salary: 1000 },
  { name: 'Tony', salary: 2000 },
  { name: 'Lily', salary: 3000 },
  { name: 'Sara', salary: 4000 }
]

找出 salary 最高的那位

let highestSalary = { salary: 0 }
for(let w of workers) {
  if(w.salary > highestSalary.salary) {
    highestSalary = w
  }
}

上面的代码那样写也没什么问题。为了深入一点,我们现在需要所有人 salary 的总和,代码如下

let sumSalary = 0
for(let w of workers) {
  sumSalary += w.salary
}

上面两个例子中都需要在循环之前定义一个变量,然后在每次循环中处理一个循环项后并更新这个变量。

为了深入理解这个循环,我们把循环内部拆解为函数,找出相似之处

// slary 对比
function greaterSlary(a, b) {
  return a.salary > b.salary ? a : b
}

function addSalary(sum, worker) {
  return sum + worker.salary
}

const initialHighest = { slary: 0 }
let temp = initialHighest
for(let w of workers) {
  temp = greaterSlary(temp, w)
}
const highestSalary = temp

const initialSumSalary = 0
temp = initialSumSalary
for(let w of workers) {
  temp = addSalary(temp, w)
}
const sumSalary = temp

上面的代码两个循环非常相似,唯一不同的是初始值和循环中的调用的函数。两者都将数组最终降为单个值,所以我们自己写 reduce

function reduce(fn, initial, arr) {
  let temp = initial
  for(let item of arr) {
    temp = fn(temp, item)
  }

  return temp
}

但是 js 里面内置了 reduce ,我们直接拿来用就行了

const highestSalary = workers.reduce(greaterSlary, {slary: 0})
const sumSalary = workers.reduce(addSalary, 0)

使用了原生的 reduce 分离了循环代码后,代码的复杂度降低了很多。

Filter

map 可以很好的处理数组的每一项 reduce 可以把一个数组缩减到一个单一值

但是想处理一个数组中的多个项目呢?首先给 workers 数组加点数据

const workers = [
  { name: 'Jack', salary: 1000, gender:'m' },
  { name: 'Tony', salary: 2000, gender:'m' },
  { name: 'Lucy', salary: 2500, gender:'f' },
  { name: 'Lily', salary: 3000, gender:'f' },
  { name: 'Sara', salary: 4000, gender:'f', }
  { name: 'Kaka', salary: 5000, gender:'m' }
]

两个问题:

  1. 找出所有的女性
  2. 找出 salary 大于 2500 的所有 worker

用个普通的 for 循环:

const femaleWorkers = []
for(let w of workers) {
  if(w.gender == 'f') {
    femaleWorkers.push(w)
  }
}

const highSalaryWorkers = []
for(let w of workers) {
  if(w.salary > 2500) {
    highSalaryWorkers.push(w)
  }
}

上面两块代码唯一不同的是 if 判断,所以把 if 转换成函数

function isFemale(worker) {
  return worker.gender === 'f'
}

function isHighSlary(worker) {
  return worker.salary > 2500
}

const femaleWorkers = []
for(let w of workers) {
  if(isFemale(w)) {
    femaleWorkers.push(w)
  }
}

const highSalaryWorkers = []
for(let w of workers) {
  if(isHighSlary(w)) {
    highSalaryWorkers.push(w)
  }
}

我们在把这种只返回 true false 的函数成为 predicate (谓词函数) 然后使用 predicate 来决定数组中元素是否保留

predicate 函数抽象到一个 filter 中

function filter(predicate, arr) {
  let temp = []
  for(let item of arr) {
    if(predicate(item)) {
      temp = temp.concat(item)
    }
  }

  return temp
}

const femaleWorkers = filter(isFemale, workers)
const highSalaryWorkers = filter(isHighSlary, workers)

同样 js 也内置了 filter 函数,直接使用

const femaleWorkers = workers.filter(isFemale)
const highSalaryWorkers = workers.filter(isHighSlary)

只需要写一个功能单一而专注的过滤函数,然后调用 filter 即可。

Find

如果需要找出 Sara ,filter 是没问题的

function isSara(worker){
  return worker.name === 'Sara'
}

const Sara = workers.filter(isSara)[0]

这样的代码并不高效,因为只有一个 Sara ,找到后就可以停止查找操作了。

所以写一个 find 函数, 返回第一个符合项

function find(predicate, arr) {
  for(let item of arr) {
    if(predicate(item)) {
      return item
    }
  }
}

const Sara = find(isSara, workers)

find 函数实现非常简单,js 也内置了 find

const Sara = workers.find(isSaraworkers)

filter 一样,代码简洁且专注。

最后

使用内置的这些迭代函数

  1. 减少循环结构,代码更简洁易读
  2. 函数名更具有语义化 map reduce filter find
  3. 关注点从整个数组转为我们关心的每一项上

在上面每一个例子中,我们把问题分解,使用更小而纯粹的函数即可找到解决方案。

因为循环中总是在处理或者构建一个数组,或者两者同时在做,因此通过 js 内置的数组处理函数几乎可以消除代码中绝大多数循环,写出复杂性更低更利于维护的代码。

rccoder commented 7 years ago

很强!

justjavac commented 7 years ago

搭车,安利一篇我的知乎回答:JavaScript 函数式编程存在性能问题么?

jawil commented 7 years ago

再次遇见@justjavac前辈大神😄

hughfenghen commented 7 years ago

推荐lodash、ramda,数据结构转换超级方便,基本上不用自己写转换逻辑,理清思路找API就行了。

ty-cs commented 6 years ago

哇!惊现学长@rccoder

xuqinggang commented 6 years ago

函数式编程reduce、map、filter、find等应用实践

// 输入为state和originData变量
// 输出为:
{
    houseTypeShared: '2居_3居',
    houseTypeWhole: '2居',
};
// state和originData变量如下:
// state
const state = {
    sharedRooms: { 0: false, 1: true, 2:true },
    wholeRooms: { 2: false, 3: true },
};

// 源数据
const originData = {
    sharedRooms: [
        {
            unique: true,
            text: '不限',
            value: 'UNLIMITED',
        },
        {
            text: '2居',
            value: 'TWO',
        },
        {
            text: '3居',
            value: 'THREE',
        },
        {
            text: '3居+',
            value: 'THREE_MORE',
        },
    ],
    wholeRooms: [
        {
            unique: true,
            text: '不限',
            value: 'UNLIMITED',
        },
        {
            text: '1居',
            value: 'ONE',
        },
        {
            text: '2居',
            value: 'TWO',
        },
        {
            text: '2居+',
            value: 'TWO_MORE',
        },
    ]
};

// 利用map, reduce, filter等函数式方法,计算出结果。
/*
好处:
1.代码精简
2.map,reduce,filter等方法更具语义化
坏处:
1.初次阅读,难以阅读理解(其实写习惯,就很容易理解了)

*/

const TypeMapParamKey = {
    sharedRooms: 'houseTypeShared',
    wholeRooms: 'houseTypeWhole',
};
Object.keys(state)
    .map(type => {
        const rtText = Object.keys(state[type])
            .filter(index => state[type][index])
            .map(index => originData[type][index].text)
            .reduce((rt, text, index) =>
                (`${rt}` + (index === 0 ? '' : '_') + `${text}`),
                '',
            );
        return {
            [TypeMapParamKey[type]]: rtText,
        };
    })
    .reduce((rt, item) => Object.assign(rt, item), {})

可以看下更详细的实践与对比