Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
ccforward opened this issue Feb 27, 2017 · 6 comments
Open

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

ccforward opened this issue Feb 27, 2017 · 6 comments

Comments

@ccforward
Copy link
Owner

不用循环的 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
Copy link

rccoder commented Feb 27, 2017

很强!

@justjavac
Copy link

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

@jawil
Copy link

jawil commented Mar 8, 2017

再次遇见@justjavac前辈大神😄

@hughfenghen
Copy link

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

@ty-cs
Copy link

ty-cs commented Oct 19, 2017

哇!惊现学长@rccoder

@xuqinggang
Copy link

xuqinggang commented May 23, 2018

函数式编程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), {})

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants