为什么要学习函数式编程
函数式编程时很古老的一个概念,一个编程范式。
- 函数式随着react流行收到越来越多的关注
- Vue3也开始拥抱函数式编程
- 函数式编程可以抛弃this
- 打包过程可以更好利用tree shaking过滤无用代码
- 方便测试,方便并行处理
- 有很多库可以帮助我们进行函数式开发:lodash,underscore,ramda
什么是函数式编程
函数式编程(functional Programming FP),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)
前置知识
函数是一等公民
- 函数可以储存在变量中
- 函数可以作为参数
- 函数作为返回值
函数是一等公民是我们后面要学习高阶函数,柯里化的基础。
高阶函数
- 可以把函数作为参数传递给另一个函数
- 可以把函数作为另一个函数的返回结果
函数作为参数
function forEach(array,fn){
for(let i=0;i<array.length;i++){
fn(array[i])
}
}
function filter(array,fn){
let results=[];
for(let i=0;i<array.length;i++){
if(fn(array[i])){
results.push(array[i])
}
}
return results;
}
函数作为返回值
function once(fn){
let done = false;
return function(){
if(!done){
done=true;
fn.apply(this,arguments)
}
}
}
let pay =once(function(money){
console.log(`支付:${money}元`)
})
//只会支付一次
pay(5)
pay(3)
使用高阶函数的意义
- 抽象可以帮我们屏蔽细节,只需要关注与我们的目标
- 高级函数是用来抽象通用的问题
//面向过程
let array = [1,2,3,4]
for(let i=0;i<array.length;i++){
console.log(array[i])
}
//高阶函数
forEach(array,item=>console.log(item))
let r = filter(array,item=>item%2===0)
常用高阶函数:forEach,map,filter,reduce,some,every...
闭包
- 可以在另一个作用域中调用一个函数的内部函数并访问该函数作用域中的成员,例如上面代码中的once。
- 闭包的本质:函数在执行的时候会放到一个执行环境栈上,当函数执行完毕之后会从栈上移除,但是堆上的作用域成员(返回的那个函数)因为被外部引用不能释放(原始类型的值放在栈中,引用类型的放到堆上),因此内部函数依然可以访问外部函数的成员。
纯函数
相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
传函数就类似数学中的函数(用来描述输入和输出之间的关系),y=f(x)
lodash 就是一个纯函数的功能库,提供了对数组,数字,对象,字符串,函数等一些操作方法。
数组的slice和splice分别就是:纯函数和不纯函数
slice返回数组指定部分,不会改变原数组
splice对数组进行操作返回该数组,会改变原数组
- 函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态)
- 我们可以把一个函数的执行结果交给另一个函数去处理
纯函数的好处
可缓存,因为纯函数对相同的输入始终有相同的输出结果,所以我们可以把纯函数的结果缓存起来。
模拟一个memoize函数
function memoize(fn){
let cache={};
return function(){
let arg_str = JSON.stringify(arguments)
cache[arg_str] = cache[arg_str]||fn.apply(this.argsuments);
return cache[arg_str]
}
}
这样我们就实现了一个缓存纯函数
副作用:副作用让一个函数变得不纯,纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。
副作用来源:配置文件,数据库,获取用户输入...
柯里化(Currying)
- 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)
- 然后返回一个新的函数接收剩余参数,返回结果
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 match =_.curry(function(reg,str){
return str.match(reg)
})
const haveSpace = match(/\s+/g);
const haveNumber = match(/\d+/g);
console.log(haveSpace('hello woed'))
console.log(haveNumber('25$'))
const filter=_.curry(function(func,array){
return array.filter(func)
})
console.log(filter(haveSpace)(['john Conner','john_Donne']))
模拟_.curry实现
function curry(func){
return function curriedFn(..args){
if(args.length<func.length){//func的参数长度
return function(){
curriedFn(...args.concat(Array.form(arguments)))
//...args是当前参数 arguments是下次调用的参数
}
}
//参数相等 ,开始调用
return func(...args)
}
}
总结:
- 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
- 这是一种对函数参数的缓存
- 让函数变得更灵活,让函数的粒度更小
- 可以把多元函数转化为一元函数,可以组合使用函数产生强大功能。
函数组合
纯函数和柯里化很容易写出洋葱圈的代码h(g(f(x)))
获取数组最后一个元素在转换成大写字母,使用lodash, _.toUpper(_.first(_.reverse(array)))
函数组合可以让我们把细粒度的函数重新组合成一个新的函数来执行
- 函数就像是数据的管道,函数组合就是把这些管道组合起来,让数据穿过多个管道形成最终的结果。
- 函数组合默认是从右到左执行
lodash中的函数组合有:flow()或者flowRight(),它们都可以组合函数
flow()是从左到右
flowRight()是从右到左,使用的更多一些
const _=require('lodash');
const toUpper =s=>s.toUpperCase();
const reverse=arr=>arr.reverse();
const first = arr=>arr[0];
cons f = _.flowRight(toUpper,first,reverse)
console.log(f(['one','two','three']))
模拟flowRight方法
//多函数组合
function compose(...fns){
return function(value){
//acc初始(value)或者,上一次运行结果返回的值,fn是指当前项
return fns.reverse().reduce(function(acc,fn){
return fn(acc)
},value)
}
}
函数组合要满足组合律
例如我们既可以把 g 和 h 组合,还可以把 f 和 g 组合,结果都是一样的
// 结合律(associativity)
let f = compose(f, g, h)
let associative = compose(compose(f, g), h) == compose(f, compose(g, h))
// true
所以lodash也可以这样
const _ = require('lodash')
// const f = _.flowRight(_.toUpper, _.first, _.reverse)
// const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse)
const f = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))
console.log(f(['one', 'two', 'three']))
// => THREE
调试组合函数
const _ = require('lodash')
const f = _.flowRight(_.toUpper, _.first, _.reverse) console.log(f(['one', 'two', 'three']))
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(' '))
console.log(f('NEVER SAY DIE'))
lodash/fp
- lodash的fp模块提供了实用的对函数式编程友好的方法。
- 提供了不可变auto-curried iteratee-first data-last的方法
具体可以查看lodash的文档
下面是正常lodash和lodash/fp模块的差别
// lodash 模块
const _ = require('lodash')
_.map(['a', 'b', 'c'], _.toUpper)
// => ['A', 'B', 'C']
_.map(['a', 'b', 'c'])
// => ['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 fp = require('lodash/fp')
const f = fp.flowRight(fp.join('-'), fp.map(_.toLower), fp.split(' '))
console.log(f('NEVER SAY DIE'))
Point Free
Point Free:我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
不需要指明处理的数据
只需要合成运算过程
需要定义一些辅助的基本运算函数
使用 Point Free 的模式,把单词中的首字母提取并转换成大写
const fp = require('lodash/fp')
const firstLetterToUpper = fp.flowRight(join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), split(' '))
console.log(firstLetterToUpper('world wild web'))
// => W. W. W
本文内容 摘抄于 拉钩大前端训练营
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。