-
Notifications
You must be signed in to change notification settings - Fork 189
55.[函数式编程] 不用循环的 JavaScript #59
Description
不用循环的 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' } ]
两个问题:
- 找出所有的女性
- 找出 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 一样,代码简洁且专注。
最后
使用内置的这些迭代函数
- 减少循环结构,代码更简洁易读
- 函数名更具有语义化
mapreducefilterfind - 关注点从整个数组转为我们关心的每一项上
在上面每一个例子中,我们把问题分解,使用更小而纯粹的函数即可找到解决方案。
因为循环中总是在处理或者构建一个数组,或者两者同时在做,因此通过 js 内置的数组处理函数几乎可以消除代码中绝大多数循环,写出复杂性更低更利于维护的代码。