函数式编程的思想手写一些常用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来进行处理