函数式编程入门系列二
数组的函数式编程
数组用于遍历,用数组存储、操作和查找数据,以及转换投影数据格式
以下创建的函数可能在数组或对象的原型中,也有可能不在,我们可以通过这些函数理解其中的运行机制,而不是覆盖原生方法
1.数组的函数式方法
以下创建的所有函数称为投影函数,把函数应用于一个值并创建一个新值得过程称之为投影
map
const forEach = (array, fn) => {
for (const value of array) {
fn(value);
}
};
//map与forEach实现类似
const map = (array, fn) => {
let results = [];
for (const value of array) {
results.push(fn(value));
}
return results;
};
console.log(map([1, 2, 3], x => x * x * 10)); [10,40,90]
//常量导出
const arrayUtils={
map:map
}
export { arrayUtils };
//模块使用
import arrayUtils from 'lib';
arrayUtils.map
//或者
const map=arrayUtils.map;
let books=[
{"name":'深入浅出vue.js',"author":"李四",id:001},
{"name":"CSS权威指南","author":"张三",id:002},
{"name":"Java权威指南","author":"赵五",id:003},
{"name":"微信小程序入门","author":"王二",id:004},
]
console.log(map(books,(item)=>({title:item.name,code:item.id})))
/*
[ { title: '深入浅出vue.js', code: 1 },
{ title: 'CSS权威指南', code: 2 },
{ title: 'Java权威指南', code: 3 },
{ title: '微信小程序入门', code: 4 } ]
*/
filter函数
//书籍相关数据
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}]
}
];
//获取评分高于4.5的书籍
const filter = (array, fn) => {
let results = [];
for (const value of array) {
(fn(value)) ? results.push(value): undefined
}
return results;
}
var res = filter(apressBooks, (book) => book.rating[0] > 4.5)
console.log(res)
/*
[ { id: 111,
title: 'C# 6.0',
author: 'ANDREW TROELSEN',
rating: [ 4.7 ],
reviews: [ [Object] ] } ]
*/
2.连接操作
我们希望连接投影函数map和filter,以便获取更复杂的期望结果
//从书籍数据中获取含有title和anthor对象且评分高于4.5的对象
//方法:结合map和filter
const map = (array, fn) => {
let results = [];
for (const value of array) {
results.push(fn(value));
}
return results;
};
const filter = (array, fn) => {
let results = [];
for (const value of array) {
(fn(value)) ? results.push(value): undefined
}
return results;
}
let data1 = filter(apressBooks, item => item.rating[0] > 4.5);
let data2 = map(data1, book => ({
title: book.title,
author: book.author
}))
console.log(data2) //[ { title: 'C# 6.0', author: 'ANDREW TROELSEN' } ]
map
和filter
都是投影函数,他们对数组应用转换操作(通过传入高阶函数)后再返回新的数据,所以我们可以连接两个函数完成上面的任务
任务:map基于评分高于4.5的数据,返回了带有title,author字段的对象
//此时需要注意map和filter的顺序
map(filter(apressBooks, item => item.rating[0] > 4.5), book => ({
title: book.title,
author: book.author
}))
concatAll函数
//concatAll函数
let apressBooks2 = [{
name: "beginners",
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
}]
}
]
}
];
const map = (array, fn) => {
let tempArr = [];
for (let value of array) {
tempArr.push(fn(value))
}
return tempArr;
}
console.log(map(apressBooks2, (book) => book.bookDetails))
/*
返回数据包含了数组中的数组,如果继续使用filter,将无法过滤,因为filter不能再嵌套数组中运行
[ [ { id: 111,
title: 'C# 6.0',
author: 'ANDREW TROELSEN',
rating: [Array],
reviews: [Array] },
{ id: 222,
title: 'Efficient Learning Machines',
author: 'Rahul Khanna',
rating: [Array],
reviews: [] } ],
[ { id: 333,
title: 'Pro AngularJS',
author: 'Adam Freeman',
rating: [Array],
reviews: [] },
{ id: 444,
title: 'Pro ASP.NET',
author: 'Adam Freeman',
rating: [Array],
reviews: [Array] } ] ]
*/
//主要作用是将嵌套数组转换为非嵌套的单一数组(降维操作)
const concatAll = (array, fn) => {
let results = [];
for (const value of array) {
results.push.apply(results, value)
//遍历时把内部二层数组通过push保存到结果数组中
}
return results;
}
console.log(
concatAll(map(apressBooks2, (book) => book.bookDetails))
)
/*
[ { id: 111,
title: 'C# 6.0',
author: 'ANDREW TROELSEN',
rating: [ 4.7 ],
reviews: [ [Object] ] },
{ id: 222,
title: 'Efficient Learning Machines',
author: 'Rahul Khanna',
rating: [ 4.5 ],
reviews: [] },
{ id: 333,
title: 'Pro AngularJS',
author: 'Adam Freeman',
rating: [ 4 ],
reviews: [] },
{ id: 444,
title: 'Pro ASP.NET',
author: 'Adam Freeman',
rating: [ 4.2 ],
reviews: [ [Object] ] } ]
*/
const filter = (array, fn) => {
let results = [];
for (const value of array) {
(fn(value)) ? results.push(value): undefined
}
return results;
}
console.log(
filter(
concatAll(map(apressBooks2, (book) => book.bookDetails)),
(book)=>book.rating[0]>4.5
)
)
/*
[ { id: 111,
title: 'C# 6.0',
author: 'ANDREW TROELSEN',
rating: [ 4.7 ],
reviews: [ [Object] ] } ]
*/
说明:
results.push.apply(results, value)
Function.prototype.call 和 Function.prototype.apply 都是非常常用的方法
apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合, 这个集合可以为数组,也可以为类数组 , apply 方法把这个集合中的元素作为参数传递给被调用的函数。
var fn=function(a,b){console.log(a,b)};
fn(1,2)
fn.call(this,1,2);
fn.apply(this,[1,3])
3.reduce函数
//数组求和
let arr=[2,3,4,5,1];
const forEach = (array, fn) => {
let i;
for (i = 0; i < array.length; i++) {
fn(array[i]);
}
};
let temp=0; //累加器开始
forEach(arr,(item)=>temp+=item);
console.log(temp) //15
思考:如果计算数组的乘积,我们就要把temp设置为1,是不是很麻烦?
这种将数组每一项遍历生成最后一个单一结果的过程,我们称之为归约数组
reduce函数改写
//reduce函数写法
const reduce=(array,fn)=>{
let accumlator=0;
for(const value of array){
accumlator=fn(accumlator,value);
}
return [accumlator]
}
console.log(reduce(arr,(acc,val)=>acc*val)) //0 原因是累加器的初始值为0
//考虑初始累加器值, 进一步改写
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]
}
console.log(reduce([1, 2, 3, 4], (acc, val) => acc * val, 1)) //[24]
console.log(reduce([1, 2, 3, 4], (acc, val) => acc * val)) //[24]
案例
let apressBooks3 = [{
name: "beginners",
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
}]
}
]
}
];
//统计评价为good和excellent的数量
const map = (array, fn) => {
let tempArr = [];
for (let value of array) {
tempArr.push(fn(value))
}
return tempArr;
}
//主要作用是将嵌套数组转换为非嵌套的单一数组
const concatAll = (array, fn) => {
let results = [];
for (const value of array) {
results.push.apply(results, value)
//遍历时把内部二层数组通过push保存到结果数组中
}
return results;
}
//获取内部属性中的数组,然后合并成在一个层级
let bookDetails = concatAll(
map(apressBooks3, item => item.bookDetails)
)
let res = reduce(bookDetails, (acc, bookDetails) => {
let goodReviews = bookDetails.reviews[0] != undefined ? bookDetails.reviews[0].good : 0;
let excellentReviews = bookDetails.reviews[0] != undefined ? bookDetails.reviews[0].excellent : 0;
return {
good: acc.good + goodReviews,
excellent: acc.excellent + excellentReviews
}
}, {
good: 0,
excellent: 0
//累加器初始值
})
console.log(res) //[ { good: 18, excellent: 24 } ]
4.zip函数
我们实际的数据是后台给我们的,假如后台给我们的数据都是分片段的,我们如何结合这些数据来使用?
// 各类书籍的reviews被分割到了另一个数组中,怎么处理这些分割的数据?
let apressBooks = [{
name: "beginners",
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],
"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]
}
]
}
];
let reviewDetails = [{
"id": 111,
"reviews": [{
good: 4,
excellent: 12
}]
},
{
"id": 222,
"reviews": []
},
{
"id": 333,
"reviews": []
},
{
"id": 444,
"reviews": [{
good: 14,
excellent: 12
}]
}
]
/**
*
*
* @description 根据fn获取array内容
* @param {*} array
* @param {*} fn
* @returns
*/
const map = (array, fn) => {
let tempArr = [];
for (let value of array) {
tempArr.push(fn(value))
}
return tempArr;
}
/**
*
* @description 将嵌套数组转换为非嵌套的单一数组
* @param {*} array
* @param {*} fn
* @returns
*
*/
const concatAll = (array, fn) => {
let results = [];
for (const value of array) {
results.push.apply(results, value)
//遍历时把内部二层数组通过push保存到结果数组中
}
return results;
}
/**
*
*
* @description 合并两个给定的数组
* @param {*} leftArr
* @param {*} rightArr
* @param {*} fn
*/
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
}
//假设将a,b数组内容相加
var a=[1,2,3];
var b=[2,3,5,7];
console.log(zip(a,b,(x,y)=>x+y)); //[3,5,8]
//统计书籍评价为good和excellent总数, 前提需要合并这两个不同数组结构
let bookDetails=concatAll(map(apressBooks,item=>item.bookDetails))
let resData=zip(bookDetails,reviewDetails,(book,review)=>{
if(book.id===review.id){
//改变默认的book对象将影响bookDetails的内容
//所以一旦创建clone对象我们就添加一个description属性
let clone=Object.assign({},book);
clone.description=review
return clone;
}
})sddsds
console.log(resData);
/*
[ { id: 111,
title: 'C# 6.0',
author: 'ANDREW TROELSEN',
rating: [ 4.7 ],
description: { id: 111, reviews: [Array] } },
{ id: 222,
title: 'Efficient Learning Machines',
author: 'Rahul Khanna',
rating: [ 4.5 ],
reviews: [],
description: { id: 222, reviews: [] } },
{ id: 333,
title: 'Pro AngularJS',
author: 'Adam Freeman',
rating: [ 4 ],
reviews: [],
description: { id: 333, reviews: [] } },
{ id: 444,
title: 'Pro ASP.NET',
author: 'Adam Freeman',
rating: [ 4.2 ],
description: { id: 444, reviews: [Array] } } ]
*/
柯里化与偏应用
1.一些术语
1.1 一元函数
const identity=(x)=>x;
1.2 二元函数
const add=(x,y)=>x+y;
1.3 变参参数
function print(a){
console.log(a);
console.log(arguments)
}
print(1,2,3)
// 1
//{ '0': 1, '1': 2, '2': 3 }
//采用es6语法的扩展运算符捕获参数
const variadic=(a,...variadic)=>{
console.log(a);
console.log(variadic)
}
variadic(1,2,3)
//1
//[2,3]
2.柯里化(Curry)
柯里化是指把一个多参数函数转换为一个嵌套的一元函数的过程
//二元函数
const add=(x,y)=>x+y
add(1,1) //2
//add函数柯里化后
// function addCurried(x){
// return function(y){
// return x+y;
// }
// }
const addCurried=x=>y=x+y;
addCurried(4)(4) //8
2.1 curry函数
const add=(x,y)=>x+y
//只能处理二元函数
const curry = (binaryFn) => {
return function (firstArg) {
return function (secondArg) {
return binaryFn(firstArg, secondArg)
}
}
}
let autoCurriedAdd=curry(add);
console.log(autoCurriedAdd(2)(2));
2.2 柯里化用例:
//创建table2,table3,table3
const table2=(y)=>2*y;
const table3=(y)=>3*y;
const table3=(y)=>4*y;
//统一方法并使用
const genericTable=(x,y)=>x*y;
table2_item1=genericTable(2,1)
table2_item2=genericTable(2,2)
table2_imte3=genericTable(2,3)
//curry化表格函数
//curry化表格函数
const table2=curry(genericTable)(2);
const table3=curry(genericTable)(3);
const table4=curry(genericTable)(4);
console.log('table is currying');
console.log(table2(2)) // 2*2
console.log(table3(2)) // 3*2
console.log(table4(2)) // 4*2
2.3 日志函数案例
开发者编写代码的时候会在应用的不同阶段编写很多日志
const loggerHelper=(mode,initialMessage,errorMessage,lineNo)=>{
if(mode==='DEBUG'){
console.debug(initialMessage,errorMessage+"at line"+lineNo)
}else if(mode==='ERROR'){
console.error(initialMessage,errorMessage+"at line"+lineNo)
}else if(mode==='WARN'){
console.warn(initialMessage,errorMessage+"at line"+lineNo)
}else{
throw 'Wrong mode'
}
}
loggerHelper('ERROR','Error At Stats.js',"Invalid argument passed",23)
loggerHelper('ERROR','Error At Stats.js',"slice is not undefined",31)
//此时调用我们重复使用了参数 mdoe和initialMessage,我们需要重新定义一个curry函数
添加类型判断以及传递参数
//添加参数类型判断
let curryFunc = (fn) => {
if (typeof fn != 'function') {
throw Error('No function provided');
} else {
//返回一个变参函数,
return function curriedFn(...args) {
return fn.apply(null, args)
}
}
}
const multiply = (x, y, z) => x * y * z;
console.log(curryFunc(multiply)(1, 2, 3)) //6
//把多参数函数转换为一元函数的curry函数
let curryFuncTpl = (fn) => {
if (typeof fn != 'function') {
throw Error('No function provided');
} else {
return function curriedFn(...args) { //返回一个变参函数,如果参数个数小于最初的fn.length,则递归调用,继续收集参数
if (args.length < fn.length) {
return function () {
return curriedFn.apply(null, args.concat([].slice.call(arguments))) //concat连接一次传入的一个参数
}
}else{
return fn.apply(null,args)
}
}
}
}
console.log(curryFuncTpl(multiply)(1)(2)(3)) //6
const multiply2 = (x, y, z,m)=>x+y+z+m;
console.log(curryFuncTpl(multiply2)(1)(2)(3)(4)) //10
3.柯里化小案例
1.在数组内容中查找数字,返回包含数字的数组内容
//无需柯里化时,我们可以如下实现。
let match = curryFuncTpl(function (expr, str) {
return str.match(expr);
});
let hasNumber = match(/[0-9]+/)
let filter = curryFuncTpl(function (f, ary) {
return ary.filter(f);
});
let findNumbersInArray = filter(hasNumber)
console.log("Finding numbers via curry", findNumbersInArray(["js", "number1",'22']))
//Finding numbers via curry [ 'number1' ,'22']
- 求数组的平方
let map = curryFuncTpl(function (f, ary) {
return ary.map(f);
});
let squareAll=map((x)=>x*x)
square([1,2,3]) //[1,4,9]
4.数据流
设计的curry函数总是在最后接受数组
4.1 偏应用
假设我们在每10毫秒后做一组操作,通过setTimeout函数实现
setTimeout(()=>console.log('Do some task'),10)
setTimeout(()=>console.log('Do some task'),10)
//question:每次都传入了10,能隐藏吗?能用curry函数解决吗?
//answer:不能,因为curry函数参数列表是从最左到最右的
我们要根据需要传递参数,并将10保存为常量,所以不能使用curry函数,一个变通的方法就是封装setTimeout函数,这样函数参数就会变成最右边的一个
const setTimeoutWapper=(time,fn)=>{
setTimeout(fn,time)
}
// const delayTenMs=curryN(setTimeoutWapper)
//console.log(delayTenMs(()=>console.log('Do some task')))
console.log(curryN(setTimeoutWapper)(10)(()=>console.log('Do some task')))
//缺点:我们不得不创建setTimeoutWapper这种封装器,这是一种开销
4.2 实现偏函数
偏函数允许我们部分的应用函数参数
const partial = function (fn,...partialArgs){
let args = partialArgs.slice(0);
return function(...fullArguments) {
let arg = 0;
for (let i = 0; i < args.length && arg < fullArguments.length; i++) {
if (args[i] === undefined) {
args[i] = fullArguments[arg++];
}
}
return fn.apply(this, args);
};
};
//使用偏函数
let delayTenMsPartial = partial(setTimeout,undefined,10);
delayTenMsPartial(() => console.log("Do X. . . task"))
delayTenMsPartial(() => console.log("Do Y . . . . task"))
5.组合与管道
函数式编程其实就是组合函数,利用一些小函数来构建一个新函数
函数式组合在函数式编程中被称为组合(composition)
5.1 组合的概念
函数式编程遵循了Unix的理念:
1. 每个程序只做好一件事 ,为了完成一个新的需求,'重新构建'好于'在复杂的旧程序中添加新的属性',这正是我们创建函数时秉承的理念,我们之前讨论的函数都应该接受一个参数并返回数据.
2. 每个程序的输出应该是另一个尚未可知的程序的输入
操作unix平台的命令
cat test.txt //cat紧跟文件位置,并输出test.txt内容
//输出 hello world
cat test.txt | grep 'world' //cat返回文本内容后再给grep执行一次搜索
car test.txt | grep 'world' | wc //计算单词world在给定的文本文件中的出现次数
管道符|
将左侧的函数输出作为输入发送给右边的函数,该处理过程就称为管道.
随着需求的随时加入,我们通过基础函数创建了一个个新函数,也就是说组合成一个新函数
基础函数必须遵守规则
每一个基础函数都需要接受一个参数并返回数据
5.2 函数式组合
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
}]
}
];
//获取评分高于4.5的书籍,然后获取title和author字段
const map = (array, fn) => {
let results = [];
for (const value of array) {
results.push(fn(value));
}
return results;
};
const filter = (array, fn) => {
let results = [];
for (const value of array) {
(fn(value)) ? results.push(value): undefined
}
return results;
}
map(filter(apressBooks, item => item.rating[0] > 4.5), book => ({
title: book.title,
author: book.author
}))
/*
/*
[ { id: 111,
title: 'C# 6.0',
author: 'ANDREW TROELSEN',
rating: [ 4.7 ],
reviews: [ [Object] ] } ]
先过滤后获取需要的字段
[ { title: 'C# 6.0', author: 'ANDREW TROELSEN' } ]
*/
filter输出的数据被作为输入参数传递给map函数,现在我们需要通过创建一个compose函数,把一个函数的输出作为输入发送给另一个函数
//接受a,b两个参数,并返回一个接受参数c的函数
//当c调用返回函数时,参数c被函数b调用,b的输出作为a的输出
const compose=(a,b)=>(c)=>a(b(c)) //函数调用从右往左
5.3 应用compose函数
假设对一个给定的数组四舍五入求值
// let data = parseFloat("3.56");
// let number = Math.round(data);
// console.log(number);
//利用comppose函数改写
const compose = (a, b) => c => a(b(c));
let getNumber = compose(Math.round, parseFloat);
// getNumber = c => Math.round(parseFloat(c));
console.log(getNumber("3.56")); //此处调用getNumber才行
假设构建一个函数计算一个字符串中单词的数量
let splitIntoSpaces = str => str.split(" ");
let count = array => array.length;
const getWordsCount = compose(count, splitIntoSpaces);
// getWordsCount =c=>count(splitIntoSpaces(c))
console.log(getWordsCount("This is a javascript code")); //5