1. 我是皮皮虾首页
  2. 编程开发
  3. 前端

前端学习记录之一:函数式编程

本节回顾

1.函数式编程(核心思想:把运算过程抽象成函数)

2.函数相关复习(函数是一等公民,高级函数,闭包)

3.函数式编程基础(lodash,纯函数,柯里化,管道,函数组合)

4.函子(Functor,MayBe,Either,IO, Task(folktale),Monad)帮助我们控制副作用,进行异常操作,异步处理等等

一、函数式编程(Functional Programming,FP)编程范式

  • 随着React的流行受到关注,Vue3也开始使用
  • 函数式编程可以抛弃this
  • 打包的时候可以更好利用tree shaking过滤无用代码
  • 方便测试和并行处理
  • 很多库可以帮助我们进行函数式开发:lodash、underscore、ramda

思维方式:把现实世界中的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)

描述数据(函数)之间的映射

前置知识

1.函数是一等公民

函数可以存储在变量中

函数可以作为参数

函数可以作为返回值

2.高阶函数

可以帮我们屏蔽细节,只需要关注我们的目标,高阶函数式用来抽象通用的问题,代码更简洁

  • 可以把函数作为一个参数传给另一个函数

eg:实现一个filter函数

// 自主实现filter
function filter (arr, fn) {
    let result = []
    for(let i=0; i<arr.length; i++) {
        if(fn(arr[i])) {
            result.push(arr[i])
        }
    }
    return result
  • 函数作为返回值

eg:实现一个once函数

// 实现一个once函数 只执行一次
function once (fn) {
    let done = true
    return function() {
        if(true) {
            if(done) {
                done = false
                return fn.apply(this, arguments)
            }
        }    
    }  
} 
let pay = once((money)=> {
    console.log('支付了' + money + '元')
})
pay(3)
pay(5)
pay(6)
pay(8)

常用高阶函数: foreach,map,filter,every,some,filter,find/findIndex,reduce,sort……

// every
function every(arr,fn) {
    for(var i=0; i<arr.length; i++) {
        if(!fn(arr[i])) {
            return false
            // break
        }
    }
    return true
}
console.log(every([1,2,3,4,5], (i)=>{return i<=10}))
// map
function map(arr, fn) {
    let result = []
    for(var i=0; i<arr.length; i++) {
        result.push(fn(arr[i]))
    }
    return result
}
console.log(map([1,2,3,4,5,9], (i)=>{return i*3}))

3.闭包

  • 函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包
  • 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员

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

好处:延长了外部函数内部变量的作用范围

案例:1.n的n次方;2.工资的等级

function makePower (power) {
    return function(num) {
        return Math.pow(num, power)
    }
}
const fn2 = makePower(2)
const fn3 = makePower(3)
console.log(fn2(3))
console.log(fn3(2))

// 求员工工资,不同级别的工资和绩效工资不同
// getSalary(10000, 2000)
// getSalary(12000, 3000)
// getSalary(15000, 4000)
function getSalary (base) {
    return function(merits) {
        return base + merits
    }
}
const level1 = getSalary(10000)
const level2 = getSalary(20000)
console.log(level1(3000))

1.纯函数

概念:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用

  • lodash是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等一些方法,函数柯里化方法等

纯函数(eg:slice e 不会改变原数组)和不纯函数(eg:splice会改变原数组)

  • 不会保留计算中间的结果,所以变量是不可变的(无状态的)
  • 我们可以把一个函数的执行结果交给另一个函数去处理不会改变原数组
  • 不会保留计算中间的结果,所以变量是不可变的(无状态的)
  • 我们可以把一个函数的执行结果交给另一个函数去处理
好处

1.可缓存(eg:带记忆的函数 模拟lodash中的memoize方法的实现)

function memorize (fn) {
    let cache = {}
    return function(){
        let code = JSON.stringify(arguments)
        cache[code] = cache[code] || fn.apply(fn, arguments)
        return cache[code]
        // return fn.apply(fn, arguments)
    }
}
let getResult = memorize(function(x, y){
    console.log(999999)
    return x*y*2
})
console.log(getResult(5, 8))
console.log(getResult(5, 8))
console.log(getResult(5, 8))

2.可测试(始终有输入输出,在断言结果)

3.并行处理

在多线程情况下并行操作共享的数据(eg:全局变量)的时候可能会发生意外情况

纯函数不需要访问共享的内存数据,所以并行环境下可以任意运行纯函数(web worker)

副作用

没有任何可观察的副作用。如果函数依赖于外部的状态就无法保证输出相同

来源:1.配置文件;2.数据库;3.获取用户的输入等外部交互……

副作用会使得方法通用性下降不适合扩展和可重用性,同时会给程序带来安全隐患和不确定性,但副作用不可能完全禁止,只能把他们置于可控范围内

柯里化

eg:使用柯里化来解决硬编码的问题。checkAge

function checkAge (min, age) {
    return min<=age
}
console.log(checkAge(18, 25))
// 函数的柯里化-闭包
function checkAge2 (min) {
    return function(age) {
        return min>=age
    }
}
// 函数的柯里化-es6
let checkAge3 = min=>  age => min>=age
let teenager = checkAge3(18)
console.log(teenager(16))
  • 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)
  • 然后返回一个新的函数接受剩余的函数,返回结果

eg:模拟lodash中的柯里化

// 模拟lodash中的柯里化函数  curry
// 功能: 创建一个函数,该函数接受一个或多个func的参数,
// 如果func所需要的参数都被提供则执行func,并返回执行的结果,否则继续返回该函数并等待接受剩余参数。
// 参数:需要柯里化的函数,返回值:柯里化后的函数
function getSum (a, b, c, d) {
    console.log(a+b+c+d)
    return a+b+c+d
}
// getSum(1, 2, 3, 4)
function curry (func) {
    return function curriedFn (...args) {
        // 形参与实参个数做比较
        if(args.length < func.length) {
            return function () {
                // 等待参数传递完成后,把参数合并再执行返回函数
                return curriedFn(...args.concat(Array.from(arguments)))
            }          
        }
        return func(...args)
    }   
}
let curried = curry(getSum)
console.log(curried(1,2)(3,4))

总结

  • 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新的函数
  • 这是一种对函数参数的‘缓存’
  • 让函数变得更加灵活,函数粒度更小
  • 可以把多元函数转换成一元函数,可以组合成功能更强大的函数

函数组合

纯函数和柯里化很容易写出洋葱代码,一层包一层。类似于h(g(f(x)))

管道:大函数拆分成小函数

fn = compose(f1, f2, f3)

b=fn(a)

概念

如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数

// 函数组合演示
function compose (f1, f2) {
    return function (e) {
      return f1(f2(e))
    }
}
function reverse(arr) {
    return arr.reverse()
}
function first (arr) {
    return arr[0]
}
let myCom = compose(first, reverse)
console.log(myCom([1,3,4,5]))

默认从右到左执行

组合函数的实现原理

eg: 模拟lodash中的组合函数

// 模拟lodash中的组合函数
function composition (...args) {
    return function(value) {
        return args.reverse().reduce((x,y) => { return y(x) }, value)
    }
}
// 箭头函数优化
const composition = (...args) => value => args.reverse().reduce((x,y)=>  y(x), value)
// 加一个操作函数
function toUpper(arr) {
    return arr.toUpperCase()
}
const f = composition(toUpper,first,reverse)
console.log(f(['this', 'is', 'me']))

结合律

我们既可以把f1,f2函数组合,还可以把f2和f3组合,结果都是一样的

const a = composition(toUpper,composition(first,reverse))
// const b = composition(composition(toUpper,first),reverse)
console.log(a(['this', 'is', 'me']))

调试

fp模块(function programmer)

提供了实用的对函数式编程友好的方法

提供了不可变 auto-curried literatee-first data-last 的方法

自动柯里化,函数优先 数据滞后

lodash和lodash/fp 模块中map方法的区别 (eg:用map把数组的中的项转为数字)

Point Free

概念

我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数

只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数

  • 不需要指明处理的数据
  • 只需要合成运算过程
  • 需要定义一些辅助的基本运算函数

函数组合也是Point Free的思想

案例

eg:把一个字符串中的首字母提取并转换成大写,使用.作为分隔符。world wild web ===> W.W.W

// 普通写法
function findFirst (value) {
    let arr = value.split(' ')
    let result = []
    arr.forEach(element => {
        result.push(element.slice(0,1))
    })
    console.log(result.join('.'))
}
findFirst('world wild web')

// Point Free概念写法
function _split(value) {
    return value.split(' ')
}
function _first(value) {
    let result = []
    value.forEach(element => {
        result.push(element.slice(0,1))
    })
    return result
}
function _join(value) {
    return value.join('.')
}
function log (value) {
    console.log(value)
    return value
}
let logFn = curry((tag, value)=>{
    console.log(tag, value)
    return value
})
const splitFirstJoin = composition(curry(_join),logFn('tag2'),curry(_first),logFn('tag1'),curry(_split))
console.log(splitFirstJoin('world wild web'))

Functor 函子

class和function

es6中的class是js原型继承的一种语法糖。js中的class就是一种特殊的function

class与function的相同和异同点

1.先声明后使用,不能重复定义,存在变量提升,但是无法初始化,class调用必须通过new关键字

ES6引入了Class(类)这个概念,通过class关键字可以定义类。constructor是一个构造方法,用来接收参数,constructor方法默认返回实例对象this,但是也可以指定constructor方法返回一个全新的对象,让返回的实例对象不是该类的实例

constructor外声明的属性都是定义在原型上的,可以称为原型属性

概念

建立在数学的范畴论的基础上

把函数式编程中的副作用控制在可控范围之后,还可以处理一些异常和异步操作

容器:包含值和值的变形关系(这个变形关系就是函数)

函子:是一个特殊的容器,通过一个普通的对象来实现。该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)

class Container {
    constructor (value) {
        this._value = value
    }
    map (fn) {
        return new Container(fn(this._value))
    }
}
let r = new Container(4)
console.log(r.map(x => x + 1).map(x => x * x))

// 对象不想每次都用new去创建,太面向对象了,用一个静态方法去new
class Container {
    static of (value) {
        return new Container(value)
    }
    constructor (value) {
        this._value = value
    }
    map (fn) {
        return new Container(fn(this._value))
    }
}
let r = Container.of(4).map(x => x + 1)
console.log(r)

实际函子

maybe函子

对外部空值的情况做处理,控制副作用在合理范围内

class MayBe {
    static of (value) {
        return new MayBe(value)
    }
    constructor (value) {
        this._value = value
    }
    map (fn) {
        return this.unRegu() ? MayBe.of(null) : MayBe.of(fn(this._value))
    }
    unRegu () {
        return this._value === null || this._value === undefined
    }
}
// {'length': 7}
let isIf = MayBe.of(undefined).map(x=> x.length).map(x => null)
console.log(isIf)
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) {
        return right.of(fn(this._value))
    }
}
// let r = right.of(6).map(x=>x*x)
// let l = left.of(6).map(x=>x*x)
// console.log(l,r)
function parseJson (str) {
    try {
        return right.of(JSON.parse(str))
    } catch (error) {
        return left.of({error: error.message})
    }
}
let r = parseJson('767').map(x => x + 1)
console.log(r)
IO函子
  • 内部的_value始终是一个函数,把函数作为值来处理
  • 可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行)
  • 把不纯的操作交给调用者来处理
const composition = (...args) => value => args.reverse().reduce((x,y)=>  y(x), value)  //组合函数
class IO {
    static of(value) {
        return new IO(function () {
            // 把取值过程包装在函数中,需要值得时候再调用函数取值
            return value
        })
    }
    constructor (fn) {
        this._value = fn
    }
    map (fn) {
        // 需要把当前函子的value和传入的函数组合成一个新的函数
        return new IO(composition(fn, this._value))
    }
}
// 调用
// let r = IO.of(process).map( p => p.execPath)
// console.log(r._value())
const fs = require('fs')

// IO函子的问题
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
    })
}
let cat = composition(print, readFile)
// 嵌套函子的函数,调用的时候不方便._value()._value(), api风格累赘,可用Monad函子来改进
let m = cat('index.html')._value()._value()
console.log(m)
folktale

一个函数式编程库

folktale与lodash、ramda不同的是,他没有提供很多功能函数,只是一些函数式处理的操作,例如compose、curry,一些函子Task、Either、Maybe等

const { compose, curry } = require('folktale/core/lambda')
const { toUpper, first } = require('lodash/fp')
// 第一个参数是传入的参数个数
let f = curry(2, (x, y)=> x+y)
console.log(f(1, 2), f(1)(2))
// compose
let c = compose(toUpper, first)
console.log(c(['first', 'second']))
Task函子

处理异步任务

使用 Folktale函数(2.3.2版本)式编程库 中的Task来演示

const { task } = require('folktale/concurrency/task')
const fs = require('fs')
const {split, find} = require('lodash/fp')
function readFile (filename) {
    return task(resolver => {
        fs.readFile(filename, 'utf-8', (err, data)=> {
            if(err) {
                resolver.reject(err)
            } 
            resolver.resolve(data)
        })
    })
}
readFile('index.html')
        .map(split('\n'))
        .map(find(x => x.includes('html')))
        .run()
        .listen({
            onRejected:err=>{ console.log(err)},
            onResolved: value => {
                console.log(value)
            }
        })
Pointed函子

实现了of静态方法的函子

of方法是为了避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文Context(把值放到容器中,使用map来处理值)

// Pointed函子 实现了of静态方法  一个概念
class Pointed {
    static of(value) {
        return new Pointed(value)
    } 
    constructor (value) {
        this._value = value
    }
    map (fn) {
        return Pointed.of(fn(this._value))
    }
}
const p = Pointed.of(9)
        .map(x=>x+1)
console.log(p)
Monad函子

可以变扁的Pointed函子

一个函子如果具有join和of两个方法并遵守一些定律就是一个Monad

// Monad函子
const fs = require('fs')
const composition = (...args) => value => args.reverse().reduce((x,y)=>  y(x), value)  //组合函数
class IO {
    static of(value) {
        return new IO(function () {
            // 把取值过程包装在函数中,需要值得时候再调用函数取值
            return value
        })
    }
    constructor (fn) {
        this._value = fn
    }
    map (fn) {
        // 需要把当前函子的value和传入的函数组合成一个新的函数
        return new IO(composition(fn, this._value))
    }
    join () {
        return this._value()
    }
    flatMap (fn) {
        return this.map(fn).join()
    }
}
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
    })
}
let cat = readFile('index.html')
            .flatMap(print)
            .join()
console.log(cat)

总结

函数式编程的运算不直接操作值,而是由函子完成

函子就是一个实现了map契约的对象

我们可以把函子想象成一个盒子,这个盒子里封装了一个值

想要处理盒子里的值,我们需要给盒子的map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理

最后map方法返回一个包含新值的函子

原创文章,作者:站长,如若转载,请注明出处:https://wsppx.cn/721/%e7%bd%91%e7%bb%9c%e5%bc%80%e5%8f%91/%e5%89%8d%e7%ab%af/

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注