函数式编程

217 阅读10分钟

1.为什么要学习函数式编程

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

2.什么是函数式编程

  • 面向对象:把现实世界中的事物抽象成程序世界中的类和对象,通过封装,继承和多态演示事物的联系
  • 函数式编程:对预算过程进行抽象,将数据以输入输出流的方式封装过程

定义太抽象了,下面我们从几方面看下函数式编程

  1. 函数式一等公民
    函数可以存储在变量中
    函数作为参数
    函数作为返回值

  2. 高阶函数
    可以把函数作为参数传递给另一个函数
    可以把函数作为另一个函数的返回结果

**

3.将函数作为参数

Array.prototype.map, Array.prototype.filter和 Array.prototype.reduce ,他们接受一个函数作为参数,并应用这个函数到数组的每一个元素

模拟数组内部 map filter reduce 方法实现

**

1.map, 返回长度和原数组长度一致的新数组,不考虑异常处理,简单的写法

Array.prototype.map = function( callback ) {
   var obj = Object(this)
   var len = obj.length
   var arr = new Array(len)
   for(let key in obj) {
     arr[key] = callback(obj[key], key, obj)
   }
   return arr
}

// 运行结果
var arr = [1,2,3]
var newArr = arr.map( item => {
    return item + 1
})
// [2,3,4]

2.filter 处理callback 里的条件,返回满足条件的数组

Array.prototype.filter = function( callback ) {
  var obj = Object(this)
  var len = obj.length
  var arr = new Array()

  for(let key in obj) {
    let result = callback(obj[key], key, obj)
    if(result) {
      arr.push( obj[key] )
    }
  }
  return arr
}

// 运行结果
var arr = [1,2,3]
arr.filter((item) => {
    return item > 1
})
// [2,3]

3.reduce 接收两个参数 ,callback initValue, 当传入initValue的时候,默认就是initValue ,不传入初始值的时候默认值就是数组的第一项

Array.prototype.reduce = function( callback, initValue ) {
  var obj = Object(this)
  var accumulator 
  var k = 0
  if( initValue ) {
    accumulator = initValue
  }else{
    if( obj.length ) {
      accumulator = obj[0]
      k++
    }else{
      throw new Error('empty array with no initial value')
    }
  }
  while( k < obj.length ) {
    accumulator = callback( accumulator, obj[k], k, obj)
    k++
  }
  return accumulator
}

var arr = [1,2,3]
var newArr = arr.reduce( (accu,item, index) => {
  console.log(accu, item, index);
  return accu + item
})
console.log(newArr);
// 1,2,1
// 3,3,2
// 6

4.函数作为返回值输出

例如只执行一次的函数

function once(fn){
    var done = false
    return function() {
        if(!done) {
            done = true
            return fn.apply(this, arguments)
        }
    }
}
var ok = once(function() {})

function isType( type ) {
    return ( obj ) => {
        return Object.prototype.call(obj) === '[object ' + type + ']' 
    }
}

isType('String')('123') // true
isType('Array')([1,2,3]) // true

上面的例子就是一个高阶函数, isType 返回了一个函数

我们经常可以看到一个常见的面试题  用js实现一个无限累加函数add(1)(2)(3)(4)...

我们可以看出都是将函数作为返回值,然后接收新的参数

function add(a) {
    function sum(b) {
        a = a + b
        return sum
    }
    sum.toString = function() {
        return a
    }
    return sum
}

打印函数会自动调用toString()方法,只需要重写sum.toString  方法返回a就可以了

5.纯函数

有相同输入,永远有相同的输出,而且没有任何可观察的副作用

副作用包括

  • 打印函数
  • http请求
  • DOM查询

总体来说就是与外部环境交互的都是副作用

纯函数有一下几个有点

  1. 作为缓存

    var memorize = function(fn) {
      var cache = {}
      return function(...args) {
        var key = JSON.stringify(args)
        cache[key] = cache[key] || fn.apply(fn, ...args)
        return cache[key]
      }
    }
    var m = memorize(funciton(x){x})
    m(2)
    m(2)
    m(2)
    // 只有第一次调用了 fn ,其他都是缓存的值
    
  2. 更好的移植
    不依赖于外部环境变化,所以有更好的移植

6.函数科里化

作用:多维函数转化成一维函数,方便函数组合

当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)

然后返回一个新的函数接收剩余参数,返回结果

比如封装一个add函数

//普通写法
function add(a,b,c) {
    return a + b + c
}
// 科里化写法
function add(a) {
    return function(b) {
        return function(c) {
            return a + b + c
        }
    }
}
//所以下面这两种方式是一样的
add(1,2,3)
add(1)(2)(3)

上面的例子,我们可以看做,先将函数参数收集起来,并在最里层返回的函数处理

如果想要做正则验证手机号和邮箱最简单的方法是

function validatePhone(phone) {
    return reg.test(phone) // reg是伪代码
}
function validataEmail(email) {
    return reg.test(email)
}

但是如果再加上验证其他消息呢,又得重新封装一个验证函数,所有我们可能会这样做

function check(string, reg) {
    return reg.test(string)
}
check(phoneReg, '12345')
check(emailReg, '12345@.com')

这样封装又会稍微麻烦一些,每次验证都得输入 正则和字符串,这个时候用科里化封装

function check(reg) {
    return function(string) {
        return reg.test(string)
    }
}
var checkPhone = check(phoneReg)
var checkEmail = check(emailReg)

//这样在使用的时候
checkPhone('12345')
checkEamil('12345@aljfl.com')

这样使用的时候就会非常简洁,那我们就封装一个通用科里化函数

功能: 创建一个函数,该函数接收一个或者多个fn 的参数,如果fn所需要的参数都被提供则执行fn并返回执行结果,否则继续返回该函数并等待接收剩余的参数

function createCurry(fn, length) {
    length = length || fn.length
    return function(...args) {
        return args.length >= length
            ? fn.apply(this, args)
            : createCurry(fn.bind(this, ...args), length - args.length)
    }
}

测试一个add函数

var add = createCurry(function(a,b,c){
    console.log([a, b, c])
})
add(1,2,3)
add(1)(2)(3)
add(1,2)(3)
add(1)(2,3)

以上运行结果都是一样的

7.偏函数

局部固定一个函数的一些参数,然后产生另一个更小元的函数, 与科里化不同的是,将一个n元函数转换成一个 n - x 元函数

function partial(fn, ...args) {
  return function(...newArgs) {
     return fn.apply(this, args.concat(newArgs)) 
  }
}
var add = partial(function(a,b){
  console.log(a+b)
},1)
add(2)
// 3

函数科里化和偏函数主要用途就是用在函数组合中

8.防抖和节流

防抖和节流

1.节流

节流函数适用于函数频繁被调用的场景, 例如window.onresize()、mousemove事件、scroll等等

一般实现方案有两种

  • 第一种是用时间戳来判断时间差是否已经到了执行时间,如果是则执行,并更新时间戳,循环
  • 第二种是采用定时器,如果定时器已经存在了,回调不执行,直到定时器触发,然后重新设置定时器

1. 时间戳方式, 记录一个时间戳,比用闭包保存这个变量,超过等待时间就执行函数

function throttle(fn, wait=50){
    var timeStamp = 0
    return function() {
        var now = new Date().getTime()
        if( now - timeStamp > wait ) {
            timeStamp = now
            fn()
        }
    }
}

1比如用mousemove举例

var hand = throttle(hanlder,1000)
function hanlder() {
    console.log('我执行了')
} 
document.addEventListener('mousemove', hand)

这样就控制鼠标滑动,1秒执行一次回调函数,这样就实现了一个简单的节流函数

2.防抖

防抖函数是指某个函数在某段时间内,无论触发了多少次,都只执行最后一次,比如页面的按钮点击,提交表单等,都需要防抖,

实现方式就是利用定时器,函数第一次执行时候设定一个定时器,之后调用时候发现已经设定的定时器就清空之前的定时器,并重新设定一个定时器,如果存在没有被清空的定时器,当定时器计时结束就触发执行

function debounce(fn, wait = 200) {
    var timer = null
    return function() {
        if( timer ) {
            clearTimeout(timer)
            timer = null
        }
        timer = setTimeout(() =>{
            fn()
        }, wait)
    }
}

滚动事件触发

function handler() {
    console.log('我触发了')
}
var hand = debounce(handler, 300)
document.addEventListener('scroll', hand)

这样的效果就是滚动结束之后最后一次才会执行 handler ,其他多次触发不会执行

3. 增强版节流

刚才的节流函数有一个问题就是当鼠标停止之后回调函数最后一次没有执行,这就导致了可能鼠标位置计算不对,所以根据防抖函数可以在节流函数里面使用,这样就会保证最后一次函数执行

function throttle(fn, wait = 50) {
    var timeStamp = 0, timer = null
    return function() {
        var now = new Date().getTime()
        if(now - timeStamp > wait) {
            timeStamp = now
            fn()
        }else{
            // 增加这个函数保证函数最后一次可以执行
            if(timer) {
                clearTimeout(timer)
                timer = null
            }
            timer = setTimeout(()=>{
                timeStamp = now
                fn()
            }, wait)
        }
    }
}

9.函数的组合 compose

纯函数和函数科里化容易写出洋葱代码h(g(f(x))), 函数组合可以让我们把细粒度的函数重新组合生成一个新的函数

数据管道

输入a ------------------->    fn  -------------------------->  b输出

当  fn 比较复杂的时候,我们可以把fn 拆分成多个小函数,变成

输入a  ----------------f3 ------m---->f2 -------n----------> f1 ---------->b输出

函数组合后

fn = compose(f1, f2, f3 )
b = fn(a)

compose 将f1,f2,f3, 组合成新的函数 fn 函数组合,然后执行 fn
函数组合:如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数

函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果,函数组合默认是从右到左执行

function compose(...args) {
    return function(value) {
        return args.reverse().reduce(function(val, cur) {
            return cur(val)
        }, value)
    }
}

函数组合要满足结合律,

我们既可以把 g 和 h组合,也可以把 f 和 g 组合,结果都是一样的

let a = compose(f,g,h)
let b = compose(compose(f,g), h) == compose(f, compose(g,h))

10.Functor 函子

容器:可以想象成一个盒子,也就是一个对象,里面可以存储不同的值,暴露了接口来操作内部的值

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

new Container(5)
.map(x => x + 1)
.map(x => x * x)
// 返回一个Container对象 { _value: 36 }

函数式编程中一般不提现new 关键字,所以将new Container 封装成了  of 静态方法

Container.of(5)
.map( x => x + 1)
.map( x => x * x)
  • 函数式编程运算不直接操作值,而是由函子完成
  • 函子就是一个实现了map契约的对象
  • 我们可以把函子想象成一个盒子,盒子里面封装了一个值
  • 想要处理盒子的值,我们需要给盒子的map方法传递一个处理值的函数,由这个函数来对值就行处理
  • map 方法返回一个包含新值的 盒子 (函子)

上面的例子就实现了一个简单的链式调用

MayBe 函子

我们在编程过程中会遇到很多错误,需要对这些错误相应处理

MayBe函子的作用就是可以对外部空值情况做处理,只需要修改一下 map 方法

map(fn) {
    return this._value == null ? Container.of(null) : Container.of(this._value)
}

但是MayBe函子虽然处理了 为空情况,但是没有告诉那个 map 时候出现的 空,Either函子可以给出有效的提示信息

Either 函子

Either 两者中的任何一个, 类似于 if else的处理

异常会让函数变得不纯,Either 函子可以用来异常处理

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

function parseJSON(str) {
    var parse = JSON.parse(str)
    try{
        return Right.of(parse)
    }catch(e){
        return Left.of({err: e.message})
    }
}

parseJSON('{"name": "123"}')
.map( x => x.name.toUpperCase())

这个时候就通过这种方式判断可以处理异常,并记录下来异常的信息

IO 函子

  • IO函子中的_value是一个函数,这里把函数作为值来处理
  • IO函子可以把不纯的动作存储到 _value  中, 延迟执行这个不纯的操作,包装当前的操作
  • 把不纯的操作交给调用者来处理

Task函子

异步任务 

Pointed 函子

实现了 of 静态方法的函子

of方法是为了避免使用new 来创建对象,更深层的含义是 of 方法用来把值放到上下文 Context

Monad 函子

是可以变扁的 Pointed 函子, IO(IO(x))

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