JavaScript

函数式编程入门系列二

2019-11-26  本文已影响0人  Eastboat

数组的函数式编程

数组用于遍历,用数组存储、操作和查找数据,以及转换投影数据格式

以下创建的函数可能在数组或对象的原型中,也有可能不在,我们可以通过这些函数理解其中的运行机制,而不是覆盖原生方法

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' } ]

mapfilter都是投影函数,他们对数组应用转换操作(通过传入高阶函数)后再返回新的数据,所以我们可以连接两个函数完成上面的任务

任务: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']
  1. 求数组的平方

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

上一篇下一篇

猜你喜欢

热点阅读