坚持周总结系列第十周 (JavaScript模块一)

JavaScript模块一

函数式编程

什么是函数式编程

函数式编程,FP是编程范式之一,我们常听说的编程范式还有面向过程编程、面向对象编程。

  • 面向对象编程的思维方式:把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的联系
  • 函数式编程的思维方式:把下现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
    • 程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数
    • x --> f(联系、映射)–> y,y = f(x)
    • 函数式编程中的函数值得不是程序中的函数(方法),而是数学中的函数即映射关系,例如:y = sin(x),x和y的关系
    • 相同的输入始终要得到相同的输出(纯函数)
    • 函数式编程用来描述数据(函数)之间的映射
// 非函数式
let num1=2
let num2=3
let sum=num1+num2
console.log(sum)

// 函数式
function add(n1,n2){
    return n1+n2
}
let sum=add(2,3)
console.log(sum)

函数是一等公民

  • 函数可以存储在变量中
  • 函数可以作为参数
  • 函数可以作为返回值

在JavaScript中函数就是一个普通对象(可以通过new Function()),我们可以把函数存储到变量/数组中,它还可以作为另一个函数的参数和返回值,甚至我们可以在程序运行的时候通过new Function(args)来构造一个新的函数。

  • 把函数赋值给变量
// 把函数赋值给变量
let fn=function(){
    console.log('Hello First-class Function')
}
fn()

// 一个示例
const BlogController={
    index(posts){return views.index(posts)},
    show(post){return views.show(post)},
    create(attrs){return Db.create(attrs)},
    updata(post,attrs){return Db.updata(post,attrs)},
    destory(post){return Db.destory(post)}
}

// 优化
const BlogController={
    index:views.index,
    show:views.show,
    create:Db.create,
    update:Db.create,
    destory:Db.destory
}

高阶函数

  • 高阶函数
    • 可以把函数作为参数传递给另一个函数
    • 可以把函数作为另一个函数的返回结果
  • 函数作为参数
// forEach
function forEach(array,fn){
    for(let i=0;i<array.length;i++){
        fn(array[i])
    }
}

// filter
function filter(array,fn){
    let results=[]
    for(let i=0;i<array.length;i++){
        if(fn(array[i])){
            results.push(arrsy[i])
        }
    }
    return results
}
  • 函数作为返回值
// makeFn
function makeFn(){
    let msg='Hello function'
    return function(){
        console.log(msg)
    }
}
const fn=makeFn()

// once
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} RMB`)
})
  • 使用高阶函数的意义
    • 抽象可以帮我们屏蔽细节,只需要关注我们的目标
    • 高阶函数是用来抽象通用的问题
// 面向过程的方式
let array=[1,2,3,4,5]
for(let i=0;i<array.length;i++){
    console.log(array[i])
}

// 高阶函数
let array=[1,2,3,4,5]
forEach(array,item=>{
    console.log(item)
})

let r=filter(array,item=>item%2===0)
  • 常用高阶函数

forEach、map、filter、every、some、find/findIndex、reduce、sort...

const map=(array,fn)=>{
    let results=[]
    for(const value of array){
        results.push(fn(value))
    }
    return results
}

const every=(array,fn)=>{
    let result=true
    for(const value of array){
        result=fn(value)
        if(!result){// 有一个为false就跳出循环
            break
        }
    }
    return result    
}

const some=(array,fn)=>{
    let result=false
    for(const value of array){
        result=fn(value)
        if(result){// 有一个为true就跳出循环
            break
        }
    }
    return result
}

闭包

  • 闭包:函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。

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

  • 闭包案例

// 生成计算数字的多少次幂的函数
function makePower(power){
    return function(x){
        return Math.pow(x,power)
    }
}

let power2=makePower(2)
let power3=makePower(3)

// 第一个参数是基本工资,第二个参数是绩效工资
function makeSalary(x){
    return function(y){
        return x+y
    }
}

let salaryLevel1=makeSalary(1500)
let salaryLevel2=makeSalary(2500)

纯函数

  • 纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
  • 数组的slice和splice分别是:纯函数和不纯的函数
    • slice返回数组中的指定部分,不会改变原数组
    • splice对数组进行操作返回该数组,会改变原数组
let numbers=[1,2,3,4,5]
// 纯函数
numbers.slice(0,3)
// 不纯的函数
numbers.splice(0,3)
  • 函数式编程不会保留中间计算结果,所以变量是不可改变的(无状态的)
  • 我们可以把一个函数的执行结果交给另一个函数去处理
纯函数的好处
  • 可缓存
    • 因为纯函数对相同的输入始终有相同的输出,所以可以把纯函数的结果缓存起来
const _=require('lodash')

function getArea(r){
    return Math.PI*r*r
}

let getAeraWithMemory=_.memoize(getArea)
console.log(getAeraWithMemory(4))
  • 自己模拟一个memoize函数
function memoize(f){
    let cache={}
    return function(){
        let arg_str=JSON.stringify(arguments)
        cache[arg_str]=cache[arg_str] || f.apply(f,arguments)
        return cache[arg_str]
    }
}
  • 可测试
    • 纯函数测试更方便
  • 并行处理
    • 在多线程环境下并行操作共享的内存数据很可能出意外情况
    • 纯函数不需压访问共享的内存数据,所以在并行环境下可以任意运行纯函数
副作用
// 不纯的函数
let mini=18
function checkAge(age){
    return age>=mini
}

// 纯函数(有硬编码,后续可以通过函数柯里化解决)
function checkAge(age){
    let mini=18
    return age>=mini
}

副作用会让一个函数不纯,如果函数依赖外部的状态就无法保证相同的输出结果,就会带来副作用。

副作用来源:

  • 配置文件
  • 数据库
  • 用户的输入

柯里化

// 柯里化
function checkAge(min){
    return function(age){
        return age>=min
    }
}
// ES6写法
let checkAge = min => age => age>=min
  • 柯里化
    • 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)
    • 然后返回一个新的函数接收剩余的参数,返回结果
  • lodash中的柯里化函数_.curry(func)
    • 功能:创建一个函数,该函数接收一个或多个func的参数,如果func所需要的的参数都被提供则执行func并返回执行结果。否则继续返回函数并等待接收剩余的参数。
    • 参数:需要柯里化的函数
    • 返回值:柯里化之后的函数
const _=require('lodash')
// 要柯里化的函数
function getSUm(a,b,c){
    return a+b+c
}
// 柯里化之后的函数
let curried=_.curry(getSum)
// 测试
curried(1,2,3)
curried(1)(2)(3)
curried(1,2)(3)
  • 案例
const _=requrie('lodash')

const match=_.curry(function(reg,str){
    return str.match(reg)
})

const haveSpace=match(/\s+/g)
const haveNumber=match(/\d+/g)

const filter=_.curry(function(func,array){
    return array.filter(func)
})

const findSpace=filter(haveSpace)
  • 模拟_.curry()函数的实现
function curry(func){
    return function curriedFn(...args){
        // 判断实参和形参的个数
        if(args.length<func.length){
            return function(){
                return curriedFn(...args.concat(Array.from(arguments)))
            }
        }
        // 实参和形参个数相同,调用func,返回结果
        return func(...args)
    }
}
  • 总结
    • 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
    • 这是一种对函数参数的缓存
    • 让函数变得更加灵活,让函数的粒度更小
    • 可以把多元函数转换成一元函数,可以组合使用函数产生更加强大的功能

函数组合

  • 函数组合可以让我们把细粒度的函数重新组合生成一个新的函数
  • 函数组合:如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间处理过程的函数合并成一个函数
// 组合函数
function compose(f,g){
    return function(x){
        return f(g(x))
    }
}

function first(arr){
    return arr[0]
}

function reverse(arr){
    return arr.reverse()
}

// 从右到左运行
let last=compose(first,reverse)
console.log(last([1,2,3,4]))
  • lodash中的组合函数
  • lodash中组合函数flow()或者flowRight(),它们都可以组合多个函数
  • flow()是从左到右运行
  • flowRight()是从右到左运行
const _=require('lodash')

const toUpper=s=>s.toUpperCase()
const reverse=arr=>arr.reverse()
const first=arr=>arr[0]

const f=_.flowRight(toUpper,first,reverse)
console.log(f(['one','two','three']))
  • 模拟实现flowRight()
// 多函数组合
function compose(...fns){
    return function(value){
        return fns.reverse().reduce(function(acc,fn){
            return fn(acc)
        },value)
    }
}

// ES6
const compose=(...fns)=>value=>fns.reverse().reduce((acc,fn)=>fn(acc),value)
  • 函数组合要满足结合律
// 结合律
let f=compose(f,g,h)
let associative=compose(compose(f,g),h)===compose(f,compose(g,h))// true
  • 组合函数的调试
const _=require('lodash')

const trace=_.curry((tag,v)=>{
    console.log(tag,v)
    return v
})

const split=_.curry((sep,str)=>_.split(str,sep))
const join=_.curry((sep,array)=>_.join(array,sep))
const map=_.curry((fn,array)=>_.map(array,fn))

const f=_.flowRight(join('-'),trace('map'),map(_.toLower),trace('split'),split(' '))
  • lodash/fplodash中的fp模块提供了使用的对以函数式编程友好的方法
// lodash模块
const _=require('lodash')

_.map(['a','b','c'],_.toUpper)

_.map(['a','b','c'])

_.split('Hello World',' ')

// lodash/fp 模块
const fp=require('lodash/fp')

fp.map(fp.toUpper,['a','b','c'])
fp.map(fp.toUpper)(['a','b','c'])

fp.split(' ','Hello World')
fp.split(' ')('Hello World')

const f=fp.flowRight(fp.join('-'),fp.map(_.toLower),fp.split(' '))

Point Free

我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。

  • 不需要指明处理的数据
  • 只需要合成运算过程
  • 需要定义一些辅助的基本运算函数
const f=fp.flowRight(fp.join('-'),fp.map(_.toLower()),fp.split(' '))
  • 案例演示
// 非Point Free模式
// Hello World => hello_world
function f(word){
    return word.toLowerCase().replace(/\s+/g,'_')
}

// Point Free
const fp=require('lodash/fp')

const f=fp.flowRight(fp.replce(/\s+/g,'_'),fp.toLower)
  • 使用Point Free的模式,把单词中的首字母提取并转换成大写
const fp=require('lodash/fp')

const firstLetterToUpper=fp.flowRight(
    join('. '),
    fp.map(fp.flowRight(fp.first,fp.toUpper)),
    split(' ')
)

函子

  • 容器:包含值和值得变形关系(这个变形关系就是函数)
  • 函子:是一个特殊容器,通过一个普通对象来实现,该对象有map方法,map方法可以运行一个函数对值进行处理(变形关系)
// 一个容器,包裹一个值
class Container{
    // of 静态方法,可以省略new关键字创建对象
    static of(value){
        return new Container(value)
    }
    
    constructor(value){
        this._value=value
    }
    
    // map方法,传入变形关系,将容器里的每一个值映射到另一个容器
    map(fn){
        return Container.of(fn(this._value))
    }
}

// 测试
Container.of(3)
	.map(x=>x+2)
	.map(x=>x*x)
  • 总结

    • 函数式编程的运算不直接操作值,而是由函子完成
    • 函子就是一个实现了map契约的对象
    • 我们可以把函子想象成一个盒子,这个盒子里面封装了一个值
    • 想要处理盒子中的值,我们需要给盒子的map方法传递一个处理值得函数(纯函数),由这个函数来对值进行处理
    • 最终map方法返回一个包含新值的盒子(函子)
  • 在函子中如果不小心传入null或undefined就会包类型错误,这时候就需要用到MayBe函子

class MayBe{
    static of(value){
        return new MayBe(value)
    }
    constructor(value){
        this._value=value
    }
	// 如果对空值变形的话直接返回值为null的函子
    map(fn){
        return this.isNothing() ? MayBe,of(null) : MayBe.of(fn(this._value))
    }
    isNothing(){
        return this._value === null || this._value === undefined
    }
}
  • 但是在MayBe函子中,我们很难确认是哪一步产生的空值问题
MayBe.of('hello world')
	.map(x=>x.tpUppeCase())
	.map(x=>null)
	.map(x=>x.split(' '))
// 返回值 MayBe {_value:null}
  • Either函子可以用来做异常处理
class Left{
    static of(value){
        return new Left(value)
    }
    constructor(value){
        this._value=value
    }
    map(fn){
        return this
    }
}

class Right{
    static of(vlaue){
        return new Right(value)
    }
    constructor(value){
        this._value=value
    }
    map(fn){
        return Right.of(fn(this._value))
    }
}

// 使用Either来处理异常
function parseJSON(json){
    try{
        return Right.of(JSON.parse(json))
    }catch(e){
        return Left.of({error:e.message})
    }
}
  • IO函子
    • IO函子中的_value是一个函数,这里是吧函数作为值来处理
    • IO函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作,把不纯的操作交给调用者来处理
const fp=require('lodash/fp')
class IO{
    static of(x){
        return new IO(function(){
            return x
        })
    }
    constructor(fn){
        this._value=fn
    }
    map(fn){
        // 把当前的value和传入的fn组合成一个新的函数
        return new IO(fp.flowRight(fn,this._value))
    }
}

// 调用
let io=IO.of(process).map(p=>p.execPath)
console.log(io._value())
  • Task 异步执行
const {compose,curry} = require('folktale/core/lambda')
const {toUpper,first} = require('lodash/fp')

// 第一个参数是传入函数的参数个数
let f=curry(2,function(x,y){
    console.log(x+y)
})

// 函数组合
let f=compose(toUpper,first)

// task
const {task}=require('folktale/concurrency/task')
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)
        })
    })
}

// 调用run执行
readFile('package.json')
	.map(split('\n'))
	.map(find(x=>x.includes('version')))
	.run().listen({
    	onRejected:err=>{
            console.log(err)
        },
    	onResolve:value=>{
            console.log(value)
        }
	})
  • Pointed函子:实现了静态of方法的函子

  • Monad函子:是一个可以变扁的Pointed函子,Monad函子具有of和join方法,并遵守一些定律

// 使用IO函子
const fs=require('fs')
const fp=require('lodash/fp')

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
    })
}

// IO(IO(x))
let cat=fp.flowRight(print,readFile)
// 调用
let r=cat('package.json')._value()._vlue()
console.log(r)

// 使用Monad函子
class IO{
    static of(x){
        return new IO(function(){
            return x
        })
    }
    constructor(fn){
        this._value=fn
    }
    map(fn){
        return new IO(fp.flowRight(fn,this._value))
    }
    join(){
        return this._vlaue
    }
    flatMap(fn){
        return this.map(fn).join()
    }
}

let r=readFile('package.json')
		.map(fp.toUpper)
		.flatMap(print)
		.join()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值