5. 数组的函数式编程
文章整理自 JavaScript ES6 函数式编程入门经典
在本章中,我们将创建一组用于数组的函数,并用函数式的方法而非命令式的方法来解决常见的问题
5.1 数组的函数式方法
本节将创建一组有用的函数,并用它们解决数组的常见问题
本节所创建的所有函数称为投影函数,把函数应用于一个值并创建一个新值的过程称为投影。讲个通俗的例子,forEach 没有返回值,所以就不是投影函数,map 有返回值,所以是投影函数
5.1.1 map
之前我们已经简单实现过 forEach,如下
const forEach = (arr,fn) => {
for(let value of arr){
fn(value)
}
}
map 的代码实现如下
const map = (array, fn) => {
let results = [];
for(let value of array){
results.push(fn(value))
}
return results
}
map 的实现和 forEach 非常相似,区别只是用了一个新的数组来捕获了结果,并从函数中返回了结果。
下面使用 map 函数来解决把数组内容平方的问题
map([1, 2, 3],(x) => x * x );
// [1, 4, 9]
如上所示,我们简单而优雅的完成了任务,由于要创建很多特别的数组函数,我们把所有的函数封装到一个名为 arrayUtils 的常量中并导出
const map = (array, fn) => {
let results = [];
for(let value of array){
results.push(fn(value))
}
return results
}
const arrayUtils = {
map:map,
}
export {arrayUtils}
// 另一个文件
import arrayUtils form 'lib'
arrayUtils.map // 使用 map
// 或者
const map = arrayUtils.map
// 如此可以直接调用 map
为了让本章的例子更具有实用性,我们要构建一个对象数组,如下
let apressBooks = [
{
'id': 111,
'title': 'c# 6.0',
'author': 'Andrew Troelsen',
'rating': [4.7],
'reviews': [{good: 4, excellent: 12}]
},
{
'id': 222,
'title': 'Efficient Learning Machines',
'author': 'Rahul Khanna',
'rating': [4.5],
'reviews': []
},
{
'id': 333,
'title': 'Pro AngularJS',
'author': 'Adam Freeman',
'rating': [4.0],
'reviews': []
},
{
'id': 444,
'title': 'Pro ASP.NET',
'author': 'Adam Freeman',
'rating': [4.2],
'reviews': [{good: 14, excellent: 12}]
},
]
本章创建的所有函数都会基于该对象数组运行。假设需要获取它,但只需要包含 title 和 author 字段。如何通过 map 函数完成?非常简单
map(apressBooks,(book) => {
return {title: book.title, author: book.author}
})
这将返回期望的结果,返回的数组中的对象只会包含 title 和 author 属性
[
{title: "c# 6.0", author: "Andrew Troelsen"},
{title: "Efficient Learning Machines", author: "Rahul Khanna"},
{title: "Pro AngularJS", author: "Adam Freeman"},
{title: "Pro ASP.NET", author: "Adam Freeman"}
]
有时候我们并不总是只想把所有的数组内容转换成一个新数组,还想过滤数组的内容,然后再做转换,下面介绍一个名为 filter 的函数
5.1.2 filter
假设我们只想获取评级高于 4.5 的图书列表,该如何做?这显然不是 map 能解决的,我们需要一个类似 map 的函数,但是把结果放入数组前判断是否满足条件
我们可以在 map 函数将结果放入数组前加入一个条件
const filter = (array, fn) => {
let results = [];
for(let value of array){
(fn(value)) ? results.push(fn(value)):undefined
}
return results
}
有了 filter 函数我们就可以以如下方式解决问题了
filter(apressBooks,(book) => {
return book.rating[0] > 4.5
})
这将返回我们期望的结果
[
{
'id': 111,
'title': 'c# 6.0',
'author': 'Andrew Troelsen',
'rating': [4.7],
'reviews': [{good: 4, excellent: 12}]
}
]
至此,我们在不断使用高阶函数改进处理数组的方式,再继续介绍下一个数组函数之前,我们将了解如何连接投影函数(map,filter),以便能在复杂的环境下获得期望的结果。
5.2 连接操作
为了达成目标,我们经常需要连接很多函数,例如,从 apressBooks 中获取含有 title 和 author 对象,且评级高于 4.5 的对象。首先,我们用之前的 map 和 filter 来做
let goodRatingBooks = filter(apressBooks,(book) => book.rating[0] > 4.5)
map(goodRatingBooks,book => {title: book.title, author: book.author})
此处要注意的是,map 和 filter 都是投影函数,因此它们总是对数组应用转换操作后再返回数据,于是我们能够连接 filter 和 map 来完成任务
map(filter(apressBooks,(book) => book.rating[0] > 4.5),book => {title: book.title, author: book.author})
上面代码描述了我们正在解决的问题:map 基于过滤后的数组(评级高于 4.5)返回了带有 title 和 author 字段的对象!
由于 map 和 filter 的特性,我们抽象出了数组的细节并专注于问题本身。
本章后面将通过函数组合完成同样的事
5.2.1 concatAll
下面对 apressBooks 对象稍作修改
let apressBooks = [
{
name: 'beginers',
bookDetails:[
{
'id': 111,
'title': 'c# 6.0',
'author': 'Andrew Troelsen',
'rating': [4.7],
'reviews': [{good: 4, excellent: 12}]
},
{
'id': 222,
'title': 'Efficient Learning Machines',
'author': 'Rahul Khanna',
'rating': [4.5],
'reviews': []
}
]
},
{
name: 'pro',
bookDetails:[
{
'id': 333,
'title': 'Pro AngularJS',
'author': 'Adam Freeman',
'rating': [4.0],
'reviews': []
},
{
'id': 444,
'title': 'Pro ASP.NET',
'author': 'Adam Freeman',
'rating': [4.2],
'reviews': [{good: 14, excellent: 12}]
}
]
}
]
现在让我回顾上一个问题,获取含有 title 和 author 对象,且评级高于 4.5 的对象。首先使用 map 函数
map(apressBooks,book => book.bookDetails)
// 返回
[
[
{
'id': 111,
'title': 'c# 6.0',
'author': 'Andrew Troelsen',
'rating': [4.7],
'reviews': [{good: 4, excellent: 12}]
},
{
'id': 222,
'title': 'Efficient Learning Machines',
'author': 'Rahul Khanna',
'rating': [4.5],
'reviews': []
}
],
[
{
'id': 333,
'title': 'Pro AngularJS',
'author': 'Adam Freeman',
'rating': [4.0],
'reviews': []
},
{
'id': 444,
'title': 'Pro ASP.NET',
'author': 'Adam Freeman',
'rating': [4.2],
'reviews': [{good: 14, excellent: 12}]
}
]
]
如你所见,map 函数返回的数据包含了数组中的数组,因为 bookDetails 本身就是一个数组,如果把上面的数据传给 filter,我们将遇到问题,因为 filter 不能在嵌套的数组上运行,这就是 concatAll 函数发挥作用的地方
concatAll 函数就是把所有嵌套数组连接到一个数组中,也可以说是数组的扁平化(flatten)方法。实现如下
const concatAll = (array,fn) => {
let results = []
for(const value of array){
results.push.apply(results,value);
}
return results
}
concatAll 的主要目的是将嵌套的数组转换成非嵌套的单一数组,下面的代码说明了这个概念
concatAll( map(apressBooks,book => book.bookDetails) )
// 返回
[
{
'id': 111,
'title': 'c# 6.0',
'author': 'Andrew Troelsen',
'rating': [4.7],
'reviews': [{good: 4, excellent: 12}]
},
{
'id': 222,
'title': 'Efficient Learning Machines',
'author': 'Rahul Khanna',
'rating': [4.5],
'reviews': []
},
{
'id': 333,
'title': 'Pro AngularJS',
'author': 'Adam Freeman',
'rating': [4.0],
'reviews': []
},
{
'id': 444,
'title': 'Pro ASP.NET',
'author': 'Adam Freeman',
'rating': [4.2],
'reviews': [{good: 14, excellent: 12}]
},
]
现在就能继续使用 filter 了
filter(concatAll( map(apressBooks,book => book.bookDetails) ), book => {
return book.rating[0] > 4.5
})
可以看到,设计数组的高阶函数可以优雅的解决很多问题
5.3 reduce 函数
reduce 函数大家应该都不陌生,比如求一个数组所有数字的和
[1,2,3,4,5].reduce((pre,cur) => pre+cur);
现在让我们自己实现一下
const reduce = (array,fn) => {
let accumlator = 0; // 累加器
for(const value of array){
accumlator = fn(accumlator,value)
}
return [accumlator];
}
// 使用方法
reduce([1,2,3,4,5],(acc,val) => acc+val)
// [15]
太棒了,但是如果我们要执行乘法呢?那么 reduce 就会执行失败,主要在于累加器初始值为 0,所以结果就是 0。
我们可以重写 reduce 函数来解决该问题,它接受一个为累加器设置初始值的参数
const reduce = (array,fn,initialValue) => {
let accumlator;
if(initialValue != undefined){
accumlator = initialValue;
}else{
accumlator = array[0];
}
if(initialValue === undefined){
for(let i = 1; i < array.length; i++){
accumlator = fn(accumlator,array[i])
}
}else{
for(const value of array){
accumlator = fn(accumlator,value)
}
}
return [accumlator];
}
我们对 reduce 函数做了修改,如果没有传递初始值,则以数组的第一个元素作为累加器的值。
现在我们尝试通过 reduce 函数解决乘积问题
reduce([1,2,3,4,5],(acc,val) => acc * val );
// [120]
现在我们要在 apressBooks 中使用 reduce。
假设有一天老板让你实现此逻辑:从 apressBooks 中统计评价为 good 和 excellent 的数量 。你想到,该问题正好可以用 reduce 函数轻松解决,我们需要先用 concatAll 将它扁平化,使用 map 取出 bookDetails 并用 concatAll 连接,如下所示
concatAll(
map(apressBooks,book => {
return book.bookDetails
})
)
现在我们用 reduce 解决该问题
let bookDetails = concatAll(
map(apressBooks,book => {
return book.bookDetails
})
)
reduce(bookDetails,(acc,bookDetail) => {
let goodReviews = bookDetail.reviews[0] != undefined ? bookDetail.reviews[0].good:0
let excellentReviews = bookDetail.reviews[0] != undefined ? bookDetail.reviews[0].excellent:0
return {good:acc.good + goodReviews,excellent:acc.excellent+excellentReviews}
},{good:0,excellent:0})
// 结果
// [{ good: 18, excellent: 24}]
我们把内部细节抽象到了高阶函数里面,产生了优雅的代码!
5.4 zip 数组
有的时候,后台返回的数据可能是分开的,例如
let apressBooks = [
{
name: 'beginers',
bookDetails:[
{
'id': 111,
'title': 'c# 6.0',
'author': 'Andrew Troelsen',
'rating': [4.7],
},
{
'id': 222,
'title': 'Efficient Learning Machines',
'author': 'Rahul Khanna',
'rating': [4.5],
}
]
},
{
name: 'pro',
bookDetails:[
{
'id': 333,
'title': 'Pro AngularJS',
'author': 'Adam Freeman',
'rating': [4.0],
},
{
'id': 444,
'title': 'Pro ASP.NET',
'author': 'Adam Freeman',
'rating': [4.2],
}
]
}
]
// reviewDetails 对象包含了图书的评价详情
let reviewDetails = [
{
'id': 111,
'reviews': [{good: 4, excellent: 12}]
},
{
'id': 222,
'reviews': []
},
{
'id': 333,
'reviews': []
},
{
'id': 444,
'reviews': [{good: 14, excellent: 12}]
}
]
这个例子中,review 被填充到一个单独的数组中,它们与书的 id 相匹配。这是数据被分离到不同部分的典型例子,那么该如何处理这些分割的数据呢?
zip 函数的任务是合并两个给定的数组,就这个例子而言,需要把 apressBooks 和 reviewDetails 合并到一个数组中,如此就能在单一的树下获取所有必须的数据,zip 实现代码如下
const zip = (leftArr,rightArr,fn) => {
let index, results = [];
for(index = 0; index < Math.min(leftArr.length,rightArr.length); index++){
results.push(fn(leftArr[index],rightArr[index]));
}
return results;
}
zip 函数非常简单,我们只需要遍历两个给定的数组,由于我们要处理这两个数组,所以需要获取它们的最小长度,然后使用当前的 leftArr 和 rightArr 值调用传入的高阶函数 fn。
假设我们要把两个数组的内容相加,可以用如下方式使用 zip
zip([1,2,3],[4,5,6],(x,y) => x+y)
// [5,7,9]
现在让我们解决之前的问题
let bookDetails = concatAll(
map(apressBooks,book => {
return book.bookDetails
})
)
let mergedBookDetails = zip(bookDetails,reviewDetails,(book,review)=>{
if(book.id === review.id){
let clone = Object.assign({},book)
clone.ratings = review
return clone
}
})
做 zip 操作时,我们接受 bookDetails 数组和 reviewDetails 数组。检查两个数组元素的 id 是否匹配,如果是,就从 book 中克隆出一个新的对象 clone,然后我们为它增加了 ratings 属性,并把 review 对象作为其值,最后,我们把 clone 对象返回。
zip 是一个小巧而简单的函数,但是它的作用非常强大
5.5 小结
今天我们又创建了一些有用的函数如 map,filter,concatAll,reduce 和 zip,让数组的操作更加容易,我们把这些函数称为投影函数,因为它们总是在应用转换操作后返回数组。
明天我们将学习函数式编程中一个非常重要的概念:函数柯里化。see you tomorrow