55.[函数式编程] 不用循环的 JavaScript
ccforward opened this issue · 6 comments
不用循环的 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
一样,代码简洁且专注。
最后
使用内置的这些迭代函数
- 减少循环结构,代码更简洁易读
- 函数名更具有语义化
map
reduce
filter
find
- 关注点从整个数组转为我们关心的每一项上
在上面每一个例子中,我们把问题分解,使用更小而纯粹的函数即可找到解决方案。
因为循环中总是在处理或者构建一个数组,或者两者同时在做,因此通过 js 内置的数组处理函数几乎可以消除代码中绝大多数循环,写出复杂性更低更利于维护的代码。
很强!
搭车,安利一篇我的知乎回答:JavaScript 函数式编程存在性能问题么?
再次遇见@justjavac前辈大神😄
推荐lodash、ramda,数据结构转换超级方便,基本上不用自己写转换逻辑,理清思路找API就行了。
函数式编程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), {})