JS之函数式编程

函数式编程的思想手写一些常用api方法

  • 手写forEach
const forEach = (arr, fn) => {
  for (let i of arr) {
    fn(i)
  }
}
  • 手写filter
const filter = (arr, fn) => {
  let lsArr = []
  for(let i of arr) {
    if (fn[i]) lsArr.push(i)
  }
  return lsArr
}
  • 手写once (比如支付,点击一次只会执行一次函数--利用闭包)
const once = (fn) => {
  let status = true
  return function() {
    if (status) {
      status = false
      return fn.apply(this, arguments)
    }
  }
}
let pay = once(function(m) {
  console.log(`这是付的钱${m}`)
})
pay(1)
  • 手写map
function map (arr, fn) {
  let lsArr = []
  for (var i = 0; i < arr.length; i++) {
    lsArr.push(fn(arr[i]))
  }
  return lsArr
}
  • 手写every(只要有一个不匹配就返回false)
const every = (arr, fn) => {
  let status = true
  for (let i of arr) {
    status = fn(i)
    if (!status) break
  }
  return status
}
  • 手写some(只要有一个匹配到就返回true)
const some = (arr, fn) => {
  let status = false
  for (let i of arr) {
    status = fn(i)
    if (status) break
  }
  return status
}
  • 手写lodash的记忆函数memoize(对执行同一个的纯函数做缓存处理)
const memoize = (fn) => {
  let lsObj = {}
  return function() {
    let key = JSON.stringify(fn)
    lsObj[key] = lsObj[key] || fn.apply(fn, arguments)
    return lsObj[key]
  }
}
  • 纯函数:对同一个函数多次执行,输出的结果相同

    • 副作用:比如在函数中用到了全局变量,或者是配置文件,数据库,用户输入等,都降低方法的可重用性,同时也会带来不安全的隐患
    • 同时,副作用不可避免
  • 闭包:在另一个作用域可以调用函数内部作用域的属性

    本质: 函数在执行时会被放到一个执行栈上,函数执行完毕会从执行栈上移除,但是如果存在外部引用,那么堆上的作用域成员就不会别释放,所以闭包的过程中内部函数依旧可以访问外部函数的成员

对柯里化的一些理解

一等公民

  • 函数也是一个对象,我们可以把函数作为一个值去处理,也就是高阶函数

柯里化

  • 柯里化概念:当一个函数有多个参数的时候,我们可以对其进行改造,可以只接受部分参数,然后return一个新函数,去接收剩余的参数,然后在返回结果,这就是函数柯里化
function checkAge(min) {
  return function(age) {
    return age >=min
  }
}
let checkAge18 = checkAge(18)
checkAge18(20) // 此时min基数为18,age为20
// 改造
const checkAge = min => age => age >= min

lodash中curry的使用(lodash中的柯里化函数)

  • curry的基本使用

可以传入一个纯函数,然后如果传入的参数是这个纯函数的所有参数,直接执行这个函数,如果传入的不是全部参数,则返回该函数并等待接收剩下的参数

const getSum = (a, b, c) => return a + b + c
const _ = require('lodash')
const curried = _.curry(getSum)
curried(1, 2, 3) // 6
curried(1, 2)(3) // 6
curried(1)(2, 3) // 6

手写lodash中的curry

const curry = (fn) => {
  // 通过展开运算符...args,来将传入的参数展开,那么args也就是放着所有的参数的数组
  return function curriedFun(...args) {
		//可以通过形参fn.length 来拿到fn函数的形参长度值
    if (args.length < fn.length) {
      // 传入的参数不全,返回一个新函数,直至传入的该函数的全部参数
      return function() {
				return curriedFun(...args.concat(Array.from(arguments)))
      }
    }
    // 此时,传入的参数是所有参数
    return fn(...args)
	}
}
  • 总结

    • 柯里化可以让我们生成一个拥有固定参数的新函数
    • 相当于对函数进行了一次缓存
    • 降低了函数的粒度
    • 将多元函数转换成一元函数
  • 案例1(字符串的match方法,正则表达式可能是不经常换的,所以可以进行柯里化处理)

''.match(/s+/g)
const match = _.curry(function(reg, str) {
  return str.match(reg)
})
haveSpace = match(/s+/g)
hanvSpace('') // 也就实现了第一行的一个重用性封装
  • 案例2(数组的过滤方法,过滤规则函数可能不是经常换的)
const filter = _.curry(function(arr, fn) {
  return arrr.filter(fn)
})
const filterSpace = filter(haveSpace)

函数组合以及函子

函数组合(compose)

  • 如果一个函数的执行要经过多个函数的处理才能得到最终结果,可以把中间的多个函数组合成一个函数
    • 也就是把数据管道从一大拆多小
    • 函数组合默认是从右往左执行的
function reverse(arr) {
  return _.reverse(arr)
}
function first(arr) {
  return arr[0]
}
// 将reverse函数和first函数组合起来
function compose(a, b) {
  return function(value) {
    return a(b(value))
  }
}

const c = compose(first, reverse)
console.log(c([1, 2, 3, 4])) // 输出结果: 4
  • lodash提供了两个组合函数
    • flowRight (从右往左执行函数)
    • flow (从左往右执行函数)
  • 手写组合函数
const compose = (...args) => (val) => args.reverse().reduce((prev, fn) => fn(prev), val)

组合函数的结合律

const one = _.flowRight(_.toUpper, _.first, _.reverse)
const two = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse)
const three = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))
  • 案例一 "HELLO WORLD" -> "hello-world"
// 因为flowRight只能传接受一个参数的函数,故给其柯里化, 下面同此
const split = _.curry((sep, str) => _.split(str, sep) 
const map = _.curry((fn, arr) => _.map(arr, fn))
const join = _.curry((sep, arr) => _.join(arr, sep))
const log = _.curry((tag, v) => {
  console.log(tab, v)
  return v
})
const c = _.flowRight(join('-'), log('map后'), map(_.toLower), log('map前'), split(' '))

lodash中fp模块

  • 因lodash中的像map,split,join等还需要我们额外柯里化一下,所以可以使用lodash提供的fp模块
const fp = require('lodash/fp')
const c = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
console.log(c("HELLO WORLD")) // 输出与案例一相同

fp模块的map和lodash普通的_.map的区别

  • lodash中_.map需要传入的参数为arr, fn, fn接到的参数是arr的每一个值,索引,arr本身,所以在假如把数组中的所有数字字符串变数组时就会出现问题
const a = ['1', '2', '3']
console.log(_.map(a, parseInt))
// 会输出[1, NAN, NAN]
/*
	因为在这里parseInt接到的参数是
	parseInt('1', 0, arr)	
	parseInt('2', 1, arr)
	parseInt('3', 2, arr)
	parseInt可以接收两个参数,第一个参数是要操作的值,第二个参数是操作成几进制
	所以数组中第二个数转变成1进制,没有,则为NAN,
*/
  • fp中的map则是第一个参数为fn,第二个参数为arr,所以fn接收的就是arr里每个值
const a = ["1", "2", "3"]
console.log(fp.map(parseInt, a))
// 输出[1, 2, 3]

point free

  • point free也就是函数组合compose
  • 案例
const fp = require('lodash/fp')
const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.toUpper, fp.first)), fp.split(' '), fp.replace(/s+/g, ' '))
console.log(firstLetterToUpper("HELLO  WORLD WWW"))

函子(functor)

  • 函子也就是functor,它是一个容器,也就是一个对象
  • 主要是为了处理副作用
  • 函子是一个具有map方法的一个对象,在函子里有一个要维护的值,但是这个值不对外公布,如果要对这个值进行处理的话,需要调用map方法,map方法接收一个处理值的函数,执行完毕返回一个新的函子,也就实现了链式调用
class Container {
  // 定义静态方法可以直接访问
  static of(value) {
    return new Container(value)
	}
  constructor(value) {
    this._value = value
	}
  map(fn) {
    return Container.of(fn(this._value)
	}
}
let r = Container.of(5).map(x => x + 2)  // 此时函子(容器)中this._value = 7

总结

  • 函数式编程的运算不直接操作值,而是由函子来完成的
  • 函子是一个实现了map契约的对象
  • 我们可以把函子看成一个盒子,这个盒子里封装着一个值
  • 想要处理盒子的值,只能通过map方法,去传递进去一个纯函数,让这个纯函数对值进行处理
  • 最终map方法返回了一个包含新值的函子(盒子)

MayBe函子

  • 如上函子,当我们传入一个null或者是undefined,此时我们在map中另其做一些操作,比如xx.toUpperCase()就会出现报错,所以我们可以采用MayBe函子的形式来进行处理
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
	}
}	

但是这样如果多个地方返回undefined或者是null的话就无法准确定位到是哪返回的

Either函子

  • Either函子更像是我们的if...else...
  • 通过try catch以及定义两个函子的方式来进行对的时候执行Right,错误的时候抛出到Left函子
class Left {
  static of (value) {
    return new Left(value)
  }
  constructor(value) {
    this._value = value
  }
  map() {
    return this
  }
}
class Right {
  static of (value) {
    return new Right(value)
  }
  constructor(value) {
    this._value = value
  }
  map(fn) {
    return Right.of(fn(this._value))
  }
}
// 测试定义一个函数
const parseJSON = (str) => {
  try {
    // 传入参数正确时会走Right函子
		return Right.of(JSON.parse(str))
  } catch (err) {
    // 反之传入参数不正确走Left函子
    return Left.of({ error: err.message })
  }
}

IO函子

  • IO函子中的_value是一个函数,把函数作为值来处理
  • 这样,就可以把不纯的函数存储到_value直接OMG,延迟执行这个函数(惰性执行)
  • 把不纯的函数交给调用者执行
const fp = require('lodash/fp')
class IO {
  // 再of静态方法中传入一个函数,但是还是为了拿到一个值
  static of (x) {
    return new IO(function() {
			return x
    })
	}
  constructor(fn) {
    this._value = fn
  }
  // 调用map,返回一个新的IO函子,并将this._value和传入map的参数合成一个函数
  map(fn) {
    return new IO(fp.flowRight(fn, this._value))
	}
}

folktale

  • folktale 是一个标准的函数式编程库
  • 提供了像compose,curry的函数处理
  • 提供了MayBe, Task, Either等函子

folktale中和lodash中相似的功能

  • compose - flowRight (这个基本一样)
const { toUpper, first, flowRight } = require('lodash/fp')
const { compose } = require('folktale/core/lambda')
let f = compose(toUpper, first)
let lF = flowRight(toUpper, first)
console.log(f(['hello', 'world']))
console.log(lF(['hello', 'world']))
  • 两边curry的区别
const { toUpper, first, curry } = require('lodash/fp')
const { curry } = require('folktale/core/lambda')
// 第一个参数为参数的个数
let f = curry(2, (x, y) => x + y)
let lF = curry((x, y) => x + y)

Task函子

Pointed函子

  • 这个函子也就是前面在普通函子中定义的of静态方法,主要是通过of方法来把值放到上下文Context中,也就是把值放到容器中,然后再通过map来进行处理

Monad函子

原文地址:https://www.cnblogs.com/Huskie-/p/14684837.html