函数式编程之数组的函数式编程

5. 数组的函数式编程

在本章中,我们将创建一组用于数组的函数,并用函数式的方法而非命令式的方法来解决常见的问题

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

猜你喜欢

转载自blog.csdn.net/zhang6223284/article/details/82725732