[函数式编程] 不用循环的 JavaScript

了解函数式编程,替代循环语句。


循环 Loop

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

1
2
3
4
5
6
7
8
9
10
11
// 所有字母大写
function capitalize(str){
return str.replace(/[a-z]/g, s => s.toUpperCase())
}

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

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

1
2
3
4
5
6
7
let i = 0
const len = company.length
const newCompany = []
while(i<len) {
newCompany.push(capitalize(company[i]))
i++
}

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

1
2
3
4
5
6
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 循环

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

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

映射 Mapping

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

如何做到更加精简

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

1
2
3
4
5
6
7
8
9
10
11
12
const company = [
'apple',
'google',
'amazon',
'facebook'
]

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

两次循环 简单粗暴:

1
2
3
4
5
6
7
8
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) ,所以可以重构下

1
2
3
4
5
6
7
8
9
10
function capitalizeArray(arr){
const newArr = []
for(let item of arr){
newArr.push(capitalize(item))
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 首字母大写
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
}

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

1
2
3
4
5
6
7
8
function map(fn, arr) {
const newArr = []
for(let item of arr){
newArr.push(fn(item))
}

return newArr
}

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

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

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

同样简单粗暴

1
2
3
4
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 的数组,包含姓名和薪水

1
2
3
4
5
6
const workers = [
{ name: 'Jack', salary: 1000 },
{ name: 'Tony', salary: 2000 },
{ name: 'Lily', salary: 3000 },
{ name: 'Sara', salary: 4000 }
]

找出 salary 最高的那位

1
2
3
4
5
6
let highestSalary = { salary: 0 }
for(let w of workers) {
if(w.salary > highestSalary.salary) {
highestSalary = w
}
}

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

1
2
3
4
let sumSalary = 0
for(let w of workers) {
sumSalary += w.salary
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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

1
2
3
4
5
6
7
8
function reduce(fn, initial, arr) {
let temp = initial
for(let item of arr) {
temp = fn(temp, item)
}

return temp
}

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

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

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

Filter

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

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

1
2
3
4
5
6
7
8
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 循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 转换成函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 中

1
2
3
4
5
6
7
8
9
10
11
12
13
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 函数,直接使用

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

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

Find

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

1
2
3
4
5
function isSara(worker){
return worker.name === 'Sara'
}

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

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

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

1
2
3
4
5
6
7
8
9
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 内置的数组处理函数几乎可以消除代码中绝大多数循环,写出复杂性更低更利于维护的代码。

欧阳森 wechat
如果需要联系我,请扫描上方二维码添加我个人微信号
坚持原创技术分享,您的支持将鼓励我继续创作!