JavaScript函数式编程

2022-08-01,,

函数编程

大多数框架和应用为了彻底解决代码重用问题都选择拥抱函数式编程,本模块介绍了本人学习函数式编程的思想以及一些常见的函数式编程库的使用和原理

#高阶函数

#函数是js中的一等公民,那么普通函数和高阶函数又有什么区别呢?主要由以下几点
- 1.高阶函数**不需要关注具体是实现细节**
- 2.高阶函数**主要用来抽象通用问题**
- 3.高阶函数**只关注目标和结果**

#函数作为参数(回调)

  • 模拟forEach

    function forEach (arr,fu){
        for(let i=0 ; i<arr.length ; i++){
            fu(arr[i]);
        }
    }
    forEach([1,2,3,4],function(item){
        console.log(item)//输出?
    })
    
  • 模拟filter

    function filter(arr,fn){
        let res = [];
        for(let i=0 ; i<arr.length ;i++){
            if(fn(arr[i])){
                res.push(arr[i])
            }
        }
        return res
    }
    let ary = filter([1,2,34,5,6],function(item){
        return item%2 === 0;
    })
    console.log(ary);//输出?
    
  • 模拟map

    const map = (arr,fn)=>{
        let res = [];
        for(let i of arr){
            res.push(fn(i))
        }
        return res;
    }
    let newAry = map([1,23,4,5,6],item=>item**2)
    console.log(newAry);//输出?
    
  • 模拟every

    const every = (arr,fn)=>{
        let res2 = true;
        for(let i of arr){
            res2 = fn(i);
            console.log(i)
            if(!res2){
               break;
            }
        }
        return res2
    }
    let ary = every([12,10,11,12,13],i=>i>10)
    console.log(ary)
    
  • 模拟some

    //some
    const some = (arr,fn)=>{
        let res = false;
        for(let i of arr){
            res = fn(i);
            if(res){
                break
            }
        }
        return res
    }
    let res3 = some ([1,3,5],(i)=>i%2 === 0);
    console.log(res3)
    

#函数作为返回值(闭包)

#闭包
可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员(也就是延长了作用域)

#闭包本质
函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能被释放,因此内部函数依然可以访问外部函数成员

#浏览器环境
在浏览器F12中可以观察以下作用域:
- 1. 执行栈:Call stack
- 2. 作用域:Scope
- 3. let,const作用域:Script 
- 4. 全局作用域:Global
  • 最简单的闭包

    function makeFn (){
        let msg = "Hello Function";
        return function () {
            console.log(msg);
        }
    }
    makeFn()() //输出?
    
  • 利用闭包实现只能支付一次的功能

    function once (fn) {
        let done = false;
        return function(){
            if(!done){
                done = true;
                return fn.apply(this,arguments)
            }
        }
    }
    
    let pay = once (function (money) {
        console.log('支付了'+ money)
    })
    pay(5)//输出?
    pay(5)//输出?
    
  • 利用闭包实现不同类型的功能函数

    const makePower = function(power){
      return function(num){
        return num ** power
      }
    }
    // a类型
    let a = makePower(2);
    // b类型
    let b = makePower(3);
    
    console.log(a(2))
    console.log(a(3))
    console.log(b(4))    
    

#lodash

Lodash是一个一致性、模块化、高性能的 JavaScript 高阶函数实用工具库。是一个意在提高开发者效率,提高JS原生方法性能的JS库。简单的说就是,很多方法lodash已经帮我们写好了。和JQ一样Lodash使用了一个简单的_符号。
npm install lodash -D #下载lodash依赖包
  • 使用方法

    const _ = require('lodash');
    const array = ['jacs','conor','lucy','kate'];
    console.log(_.first(array));
    

纯函数(memoize)

#什么是纯函数?
执行多次函数的输入和输出始终能保持一致.

#它的好处有哪些?
好处1 可缓存:因为纯函数对相同的输入始终都有相同的输出,所以它的结果可以通过闭包被缓存下来(记忆函数)
好处2 可测试:纯函数让测试更方便(因为纯函数始终有输入和输出,而单元测试就是在断言这个结果)
好处3 并行处理(web worker):在多线程环境下并行操作共享的内存数据很可能会出现意外情况. 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数

#纯函数的副作用
如果纯函数内部的变量依赖于外部的边量就会导致不纯,又如果在内部写死的话会导致硬编码.

#外部变量(副作用)的来源: 
全局变量 配置文件 数据库 获取用户的输入(跨站脚本攻击)...
所有外部的交互都可能带来副作用,副作用不可能完全禁止,但要尽可能控制它们在可控范围内发生
(ps:函子就用来解决这个问题)
  • 硬编码

    function checkAge(age){
        let min = 18; //硬编码
        return age >= min
    }
    
  • 普通纯函数

    function checkAge2(age,min){
        return age >= min //相同的输入始终有相同的输出
    }
    
  • lodash中的纯函数

    const _ = require('lodash')
    function getArea (r) {
        console.log(r);
        return Math.PI * r * r
    }
    let getAreaWithMemory = _.memoize(getArea);
    console.log(getAreaWithMemory(4));//输出?
    console.log(getAreaWithMemory(4));//输出?
    
  • 模拟纯函数memoize

    function getArea (r) {
        console.log(r);
        return Math.PI * r * r
    }
    function memoize (getArea){
      	//记忆对象
        let cache = {};
      	//闭包函数
        return function(){ 
            //是否有缓存,有就直接取缓存,没有就重新计算.
            cache[arguments] = cache[arguments] || getArea.apply(this,arguments);
            return cache[arguments]
        }
    }
    let memoizeFun = memoize(getArea)
    console.log(memoizeFun(4));
    console.log(memoizeFun(4));
    

#柯里化

#什么是柯里化
即调用一个函数只传递部分参数(这部分参数永远不变),然后返回一个新的函数并接收剩余的参数并返回结果
  • 纯函数会带来重复的代码

    function checkAge2(age,min){
        return age >= min 
    }
    //普通纯函数会带来一些重复的代码,如:
    checkAge2(18,20);
    checkAge2(18,21);
    checkAge2(18,22);
    
  • 模拟柯里化

    function checkAge3(min){
        return function(age){
            return age >= min
        }
    }
    //只传部分参数
    let age18 = checkAge3(18);
    //接收剩余的参数
    console.log(age18(22));
    console.log(age18(55));
    console.log(age18(17));
    
  • lodash中的柯里化

    lodash 中的 curry : 可以把任意多元参数的函数转化成为一个一元参数的函数,如下
    
    const _ = require('lodash');
    function sum (a,b,c){ //三个参数就是三元的意思
        return a+b+c
    }
    const curry = _.curry(sum);
    
    console.log(curry(1,2,3));
    console.log(curry(1)(2,3));
    
    //如下就是把一个三元参数的函数转为一元的函数
    const yiyuan = curry(1,2);
    console.log(yiyuan(3));
    
  • 柯里化案例-提取字符串中的空格和数字

    • 面向过程的方法

      ''.match(/\s+/g);
      ''.match(/\d+/g);
      
    • 柯里化的方法

      const _ = require('lodash');
      function match (reg,str) {
          return str.match(reg)
      }
      //生产一个柯里化函数,它接受两个参数
      const thisMatch = _.curry(match);
      
      const haveSpace = thisMatch(/\s+/g)
      console.log(haveSpace('hello word'));
      console.log(haveSpace('helloword'));
      
      const haveNum = thisMatch(/\d+/g)
      console.log(haveNum('155656'));
      console.log(haveNum('dadsdsa'));
      
  • 模拟柯里化

    //第一步 观察lodash中柯里化接受的参数 是一个函数
    function curry (fn) {
      	//第二步 lodash中返回的是一个接收参数的函数
        return function cry(...arg) {
            //第三部 判断是否满足参数个数
            if(arg.length >= fn.length){
              	//满足=>直接返回
                return fn.apply(this,arg)
            }else{
                //不满足=>返回新的函数
                return function(...arguments){
                    //第一次传进来的arg被缓存了,这里组合起来 再给cry判断一遍 如此重复直到满足为止
                    let ary = arg.concat(arguments)
                    return cry(...ary)
                }
            }
        }
    }
    const curryFun = curry(sum);
    console.log(curryFun(1,2,3));
    console.log(curryFun(1,2)(3));
    console.log(curryFun(1)(2,3));
    
  • 柯里化总结

    #优点
    1.柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定值的新函数
    2.这是一种对函数参数的缓存
    3.让函数变的更灵活,让函数的粒度更小
    4.可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能
    
    #缺点
    5.但是纯函数和柯里化又很容易写出洋葱代码
    //如: _.toUpper(_.first(_.reverse(array)))
    (ps:所以诞生了组合代码 组合代码可以把细粒度的函数重新组合生成一个新的函数)
    

#组合函数(pointFree编程风格)

#组合函数
1.如果一个函数要经过对个函数处理才能得到最终的值,这个时候可以把中间过程的函数合并成一个函数
2.函数就是数据的管道,把多个管道连接起来,让数据穿过形成最终的结果
3.函数组合默认是从右到左执行,每一个函数接收一个参数并且返回相应的结果
  • 最简单的组合函数

    //最简单的组合函数,实现取出数组中的最后一个元素
    function compose (f,g){
        return function (value) {
            return f(g(value))
        }
    }
    
    function reverse (ary){
        return ary.reverse();
    }
    
    function first (ary){
        return ary[0];
    }
    
    const c = compose(first,reverse);
    console.log(c([1,2,1,5,6]));
    //虽然取出最后一个元素有N种方法,但是要注意的是:
    //通过组合的方式可以随意组合不同的函数实现不同的功能,且复用性非常强,功能强大.
    
  • lodash中的组合函数(flowRight)

    const _ = require('lodash');
    const reverse = (arr)=>arr.reverse();
    const first = (arr)=>arr[0];
    const compose = _.flowRight(first,reverse);
    console.log(compose([1,5,6,4,8,6,'dsadasdsd']));
    
  • 函数组合要满足结合律

    //函数组合要满足结合律:(a×b)×c=a×(b×c)
    const _ = require('lodash');
    
    const reverse = (arr)=>arr.reverse();
    const first = (arr)=>arr[0];
    const upperCase = (str)=>str.toUpperCase();
    const log = (val)=>{
        console.log(val);
        return val
    }
    
    //结合律
    const compose2 = _.flowRight(_.upperCase,log,_.flowRight(_.first,log,_.reverse));
    console.log(compose2([1,5,6,4,8,6,'dsadasdsd']));
    
  • 模拟flowRight

    const reverse = (arr)=>arr.reverse();
    const first = (arr)=>arr[0];
    
    function flowRight(...arg){
        return function(value){
            let res = value;
            for(let i of arg.reverse()){
                res = i(res)
            }
            return res;
        }
    }
    //或者
    function flowRight2(...arg){
        return function(value){
            return arg.reverse().reduce(function(cur,fn){
                //console.log(cur,fn);
                return fn(cur);
            },value)
        }
    }
    const compose = flowRight(first,reverse);
    console.log(compose([1,5,6,4,8,6,'dsadasdsd']));
    
  • lodash中的fp模块

    由于lodash中提供的split,join,map等功能函数的参数位置不适合函数组合(数据优先,函数至后),这里可以用到柯里化把参数对调,并在组合函数中传入部分参数,剩余参数从右至左传入(如下). 这种方法比较麻烦,所以lodash中出现了fp模块(函数优先,数据至后)
    
    //调试组合函数
    //NEVER SAY DIE  --> never-say-die
    const _ = require('lodash');
    
    const split = _.curry((separator,string)=>_.split(string, separator));
    const join = _.curry((separator,string)=>_.join(string, separator));
    const map = _.curry((iteratee,collection)=>_.map(collection, iteratee));
    const trace = _.curry((wz,log)=>{console.log(wz,log);return log;})
    
    const compns = _.flowRight(join('-'),trace('2222'),map(_.toLower),trace('1111'),split(' '))
    console.log(compns('NEVER SAY DIE'));
    
    • fp模块的使用

      //调试组合函数 fp模块中的参数都是被柯里化过的 它只接受一个参数
      //NEVER SAY DIE  --> never-say-die
      const fp = require('lodash/fp');
      const compns = fp.flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' '))
      console.log(compns('NEVER SAY DIE'));
      
  • lodash中map的问题

    //lodash中map和fp模块汇总的map的区别
    const _ = require('lodash');
    const fp = require('lodash/fp');
    
    console.log(_.map(['25','23','10'], parseInt)); //[ 25, NaN, 2 ]
    //_.map(collection, iteratee) 因为iteratee函数默认接收三个参数 分别是(值,索引,原数组)
    //而parseInt默认接收两个参数分别是(值,进制数)
    //所以最后是这样的 parseInt('25',0) parseInt('23',1) parseInt('10',2) 不同的进制转换的数据不同,如果未传入第二个参数,则前缀为“x”的字符串被视为十六进制。所有其他字符串都被视为十进制。
    
    //所以fp模块就没有这个问题,因为他是被柯里化过的 只接收一个参数
    console.log(fp.map(parseInt)(['25','23','10']));
    

#函子

#函子:
是一个class类(对象),维护一个值,通过map方法去接收一个处理值的函数,并返回一个新的函子

#特性:
1.函数式编程的运算不直接操作值,而是由函子完成
2.函子就是一个实现了map契约的对象
3.可以把函子想象成一个盒子,这个盒子里封装了一个值
4.想要处理值,需要给map传递一个处理值的纯函数
5.最终map返回一个包含新值的函子
  • Functor函子

    class Container {
        constructor(value){
            this.value = value; //维护一个值
        }
        //通过map方法去接收一个处理值的函数
        map(fn){
          	//并返回一个新的函子,因为返回的是一个新的函子所以可以无限map对这个值进行链式编程
            return new Container(fn(this.value));
        }
    }
    const f = new Container(5).map(x=>x+1).map(x=>x-1).map(x=>x**2)
    console.log(f);
    
    • Functor函子缺点

      #问题:
      1.每次调用map的时候都要去new一下,所以需要封装一下 pointed函子解决~
      2.不能对非法的值输入值进行判断,而导致程序报错终止. 用MapBe函子解决~
      
  • Pointed函子

    of静态方法是为了避免new来创建对象,更深层的含义是用来把值放到上下文context中(就是把值放到容器中,方便使用map来处理值)
    
    class Container {
        //static 创建静态对象,可以直接通过类来调用
        static of (val) {
          	//因为返回的是一个新的函子所以可以无限map对这个值进行链式编程
            return new Container(val)
        }
        constructor(value){
            //维护一个值
            this.value = value; 
        }
        map(fn){
          	//通过map方法去接收一个处理值的函数,并返回一个新的函子
            return Container.of(fn(this.value));
        }
    }
    const fn = Container.of(5).map(x=>x+1).map(x=>x+1)
    console.log(fn);
    
  • MapBe函子

    MayBe:的作用就是可以对外部的控制情况做处理(控制副作用在允许的范围),如下就多了一个isNothing空值判断
    
    class MayBe {
        static of (value) {
            return new MayBe(value)
        }
        constructor(value) {
            this.value = value
        }
        map(fn){
            return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this.value))
        }
        isNothing(){
            return this.value === null || this.value === undefined
        }
    }
    
    console.log(MayBe.of('hello word').map(x=>x.toUpperCase())); //{ value: 'HELLO WORD' }
    console.log(MayBe.of(null).map(x=>x.toUpperCase()));         //{ value: null }
    
    • MapBe函子缺点

      #问题:
      当链式调用非常多的时候,其中一步的值置为nullMayBe函子无法准确定位被赋值为null的步数(如下). Either函数就用来处理这个问题~
      
      let r = MayBe.of('hello word')
      .map(x=>x.toUpperCase())
      .map(x=>null)
      .map(x=>x+1)
      console.log(r);
      
  • Either函子

    Either:类似于if...else 异常会让函数变的不纯,Either函子可以用来做异常处理
    
    //Either:类似于if...else 异常会让函数边的不纯,Either函子可以用来做异常处理
    class Left {
        static of (value) {
            return new Left(value)
        }
    
        constructor(value){
            this.value = value
        }
    
        map(fn){
            return this
        }
    }
    
    class Right {
        static of (value) {
            return new Right(value)
        }
    
        constructor(value){
            this.value = value
        }
    
        map(fn){
            console.log('ssss');
            return Right.of(fn(this.value))
        }
    }
    
    function parseJSON (str) {
        try {
            return Right.of(JSON.parse(str))
        }catch(err){
            return Left.of({error:err.message});
        }
    }
    
    console.log(parseJSON('{name:conor}'));
    
  • IO函子

    #IO函子:
    与之前的函子不同的是IO函子维护的value是一个函数而不是一个值,目的是把可能不纯的函数存储到value中并延迟执行它(就是交给调用者去执行,惰性执行)
    
    const _ = require('lodash/fp');
    class IO {
        static of (value) { 
          	//of方法还是接收一个值,只不过这个值把这个值封装到了函数里
            return new IO(function(){ 
                return value
            })
        }
        constructor(fn){
            this.value = fn
        }
        map(fn){
          	//所谓减少副作用既输入一定有输出,这里始终输出的是一个函数并保存到了value中
            return new IO(_.flowRight(fn,this.value)) 
        }
    }
    
    const f = IO.of(5).map(x=>x+1)
    //把可能不纯的函数存储到value中并延迟执行它(就是交给调用者去执行,惰性执行)
    console.log(f.value()); 
    
    • IO函子的缺点

      #问题描述:
      如下:当去调用嵌套函子执行结果时要.value().value(),这样书写很不友好. 可以用Monad函子来扁平化~
      
      //做一个读取文件的函数
      const fs = require('fs');
      let readFile = function(fileName){
          return new IO(function(){
              return fs.readFileSync(fileName,'utf-8')
          })
      }
      let print = function(x){
          return new IO(function(){
              console.log(x);
              return x
          })
      }
      // ff拿到的是一个嵌套的函子 ==> IO(IO)
      let ff = _.flowRight(print,readFile);
      console.log(ff('package.json'));//IO { value: [Function] }
      console.log(ff('package.json').value().value());
      
  • Monad函子

    #Monad函子
    Monad函子是可以变扁的Pointed函子,Monad主要解决函子嵌套输出问题 IO(IO(x)),
    一个函子如果具有join和of两个方法并遵守一些定律就是一个Monad,
    如果说函数嵌套可以用函数组合,那么函子嵌套就可以用Monad
    
    const _ = require('lodash/fp');
    class IO {
        static of (value) { 
          	//of方法还是接收一个值,只不过这个值把这个值封装到了函数里 
            return new IO(function(){ 
                return value
            })
        }
        constructor(fn){
          	//接受一个函数 缩小副作用
            this.value = fn 
        }
        map(fn){//map是处理值的操作
          	//所谓减少副作用既输入一定有输出,这里始终输出的是一个函数并保存到了value中
            return new IO(_.flowRight(fn,this.value)) 
        }
        join(){
          	//是配合flatMap的操作
            return this.value();
        }
        flatMap(fn){
          	//flatMap是处理嵌套函子从而避免重复.value()的操作
            return this.map(fn).join();
        }
    }
    
    • 解决IO函子读取文件的问题

      //IO函子的问题(做一个读取文件的函数)
      const fs = require('fs');
      
      let readFile = function(fileName){
          return new IO(function(){
              return fs.readFileSync(fileName,'utf-8')
          })
      }
      
      let print = function(x){
          return new IO(function(){
              //console.log(x);
              return x
          })
      }
      
      // ff拿到的是一个嵌套的函子 ==> IO(IO)
      let ff = _.flowRight(print,readFile);
      //IO { value: [Function] }
      console.log(ff('package.json'));
      
      // console.log(readFile('package.json').flatMap(print).value());
      console.log(
              readFile('package.json')
              .flatMap(print)
              .map(_.split('\n'))         
              .map(_.find(x=>x.includes('version')))
              .join()
      );
      

#Folktale函数编程库

#folktale
folktale是一个标准的函数式编程库
和lodash,ramada不同的是,他没有提供很多的功能函数
只提供了一些函数式处理的操作,如:compose,curry等,以及一些函子:Task(异步操作),Either,MayBe等.
下载: cnpm i folktale
folktale库中的 compose(函数组合和lodash中的flowRight一样),curry
  • 使用

    const {compose,curry} = require('folktale/core/lambda');
    const {first,toUpper} = require('lodash/fp');
    
    //它有两个参数,第一个参数指明后面的函数有几个参数
    const f = curry(2,(x,y)=>x+y); 
    console.log(f(1,2));
    console.log(f(1)(2));
    
    const f2 = compose(toUpper,first)
    console.log(f2(['sdsds','opps']));
    
  • Folktale之Task异步操作

    //task实现过于复杂,这边就不模拟了
    //folktale 2.x与1.x 中的task区别比较大,1.0中更接近我们现在演示的函子
    //这里以2.3.2来演示
    const {task} = require('folktale/concurrency/task');
    const _ = require('lodash/fp');
    const fs = require('fs');
    function readFile (fileName) {
        return task(resolver => {
            fs.readFile(fileName,'utf-8',(err,data)=>{
                if(err) resolver.reject(err)
                resolver.resolve(data)
            })
        })
    }
    
    readFile('package.json')
    .map(_.split('\n'))
    .map(_.find(x=>x.includes('version')))
    .run()
    .listen({
        onRejected(err){
            console.log(err);
        },
        onResolved(data){
            console.log('解析version',data);
        }
    })
    

本文地址:https://blog.csdn.net/qq_42308316/article/details/107494779

《JavaScript函数式编程.doc》

下载本文的Word格式文档,以方便收藏与打印。