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

栏目: ASP.NET · 发布时间: 6年前

内容简介:在本章中,我们将创建一组用于数组的函数,并用函数式的方法而非命令式的方法来解决常见的问题本节将创建一组有用的函数,并用它们解决数组的常见问题本节所创建的所有函数称为投影函数,把函数应用于一个值并创建一个新值的过程称为投影。讲个通俗的例子,forEach 没有返回值,所以就不是投影函数,map 有返回值,所以是投影函数

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

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


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Viral Loop

Viral Loop

Adam L. Penenberg / Tantor Media / 2009-10-27 / USD 34.99

From Google to Facebook, a respected journalist delves into how a "viral loop" can make an online business a success.一起来看看 《Viral Loop》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

在线进制转换器
在线进制转换器

各进制数互转换器

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码