函数式编程
函数式编程
-
为什么学习函数式编程
- 函数式编程是随着 react 的兴起才开始流行的,但是历史甚至早于第一台计算机。
- react 中的用了函数式编程的开发思想,但是不是全部都是使用函数式编程开发的
- react 中的高阶组件用了高阶函数来实现,高阶函数就是函数式编程的特性
- react 生态使用函数式编程的思想
- vue2.0 中也有用到函数式编程,但是在新版的 vue3.0 中重构了 vue2.0 更是用到了大量的函数式编程
-
函数式编程的优点:
- 函数式编程可以抛弃this
- 打包的过程中中可以更好的理由tree shaking 过滤无用的代码
- 方便测试,方便并行处理
有很多库可以帮助我们更好的使用函数式编程进行开发 ,如 lodash 、underscore 、ramda
1. 函数式编程概念
函数式编程 Functional Programming 已编程范式之一,我们常用的编程范式还有面向对象编程和面向过程编程
-
面向过程编程:执行步骤具体化流程化,把大段代码拆成函数,通过一层一层的函数调用 可以把复杂任务分解简化这种分解可以成为面向过程的程序设计 函数就是面向过程的程序设计。
-
面向对象 - 把现实中的食物抽象成程序世界中的类和对象,通过封装 继承和多态来处理事物和事件之间的联系。
-
函数式编程:是把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
-
程序的本质: 根据输入通过某种运算获得相应的输出
-
x ->f(通过f)-> 得到y , 可以用y=f(x)来表示
-
函数式编程中的函数不是值程序中的函数或者是方法,而是值数学中的函数即为映射关系。
-
有输入有输出,并且相同的输入要得到相同的输出。(纯函数)
-
函数式编程是用来描述数据之间的映射的。
-
总结:用来描述数据之间的映射, 即为已知一个数字通过某种运算得到一个新的数字, 对这个运算过程的抽象就是函数式编程。
// 非函数式
let a = 1;
let b = 2;
let sum = a+b;
console.log(sum);
// 函数式
function add(a,b){
return a+b
}
let sum1 = add(1,2)
console.log(sum1);
2. 高阶函数相关
2.1 函数是一等公民
First-class Function MDN头等函数说明
- 函数可以存储在变量中
- 函数可以作为参数
- 函数可以作为返回值
函数作为一个普通对象,可以进行存储作为参数和作为但会值,运行的时候可以通过new Function () 来构造新的函数。
- 函数赋值给变量
// 一个函数包裹了另一个函数,形式是一样的,我们就认为两个函数是一样的函数
const BlogController = {
index(posts) {
return Views.index(posts)
},
show(posts) {
return Views.show(posts)
},
create(posts) {
return Db.create(posts)
},
update(posts) {
return Db.update(posts)
},
destroy(posts) {
return Db.destroy(posts)
},
}
// 因为index这个方法和内部的View.index 方法的参数为都为posts,并且返回值为 View.index方法调用后的值,所以可以认为两个方法是一样的
// 上面方法可以优化为下面代码 更简洁清晰
const BlogController = {
index: Views.index,
show: Views.show,
create: Db.create,
update: Db.update,
destroy: Db.destroy,
}
2.2 高阶函数
Higher-order function
- 可以把函数作为参数传递给另一个参数
- 可以把函数作为另一个函数的返回结果
高阶函数的意义: 函数式编程的 抽象运算过程 抽象可以帮我屏蔽运算细节,专注目标 ,可以用来抽象通用问题。
// 高阶函数 函数作为参数
function forEach(arr,fn) {
for (let i = 0; i < arr.length; i++) {
fn(arr[i])
}
}
// 函数作为返回值
function makeFn(){
let msg = 'hello'
return function(){
console.log(msg);
}
}
常用的高阶函数 :forEach map filter every some find findIndex reduce sort等等
// 模拟常用高阶函数
// map
const map = (arr,fn)=>{
let res =[];
for (let i = 0; i < arr.length; i++) {
res.push(fn(arr[i]))
}
return res;
}
// let arr = [1,2,3,4]
// let res = map(arr,v=>v*v)
// console.log(res);
//every
const every = (array,fn)=>{
let res = true;
for (let i = 0; i < array.length; i++) {
res = fn(array[i])
if(!res) break
}
return res
}
// let arr = [3,4,5]
// let res = every(arr,v=>v>2)
// console.log(res);
// some
const some = (array,fn)=>{
let res = false;
for (const value of array) {
res = fn(value)
if(res) break
}
return res;
}
let arr = [1,4,5,7,9]
let res = some(arr,v=>v%2===0)
console.log(res);
2.3 闭包
- Closure 在另一个作用域中,可以调用函数的内部函数,并访问了该函数的内部成员
作用:延长了外部变量的作用范围
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`);
})
pay(18)
pay(18)
pay(18)
pay(18)
闭包的本质:函数在执行的时候会在执行栈上当函数执行完毕后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。
function makePower(power) {
return function(number){
return Math.pow(number,power)
}
}
let power2 = makePower(2);//二次方
let power3 = makePower(3);
// console.log(power2(4));
// console.log(power2(5));
// console.log(power3(4));
function makeSalary(base) {
return function(performance){
return base + performance;
}
}
let salaryLevel1 = makeSalary(12000);
let salaryLevel2 = makeSalary(15000);
console.log(salaryLevel1(2000));
console.log(salaryLevel2(3000));
3. 纯函数
概念:1. 相同的输入始终会得到相同的输出(基于纯函数的概念可以把纯函数的结果进行缓存,减少函数调用执行,增加效率)
- 纯函数式函数式编程的核心,类似数学中的函数用来描述输入和输出之间的关系
- 函数式编程不会保留中间的结果,所以变量是不可变的,也是无状态的
- 我们也可以报一个函数的执行结果交给另一个函数去处理
数组的slice就是纯函数而splice方法不是纯函数因为slice不会改变原数组,而splice会改变原数组,影响输入和输入的一致性。
// 纯函数和不纯的函数
// slice splice
let arr = [1,2,3,4,5,6,7]
// 纯函数
console.log(arr.slice(0,3));//[ 1, 2, 3 ]
console.log(arr.slice(0,3));//[ 1, 2, 3 ]
console.log(arr.slice(0,3));//[ 1, 2, 3 ]
// 不纯的函数
console.log(arr.splice(0,3));//[ 1, 2, 3 ]
console.log(arr.splice(0,3));//[ 4, 5, 6 ]
console.log(arr.splice(0,3));//[ 7 ]
// 纯函数
function getSum(a,b){
return a+b
}
console.log(getSum(1,2));
console.log(getSum(1,2));
console.log(getSum(1,2));
纯函数的好处:
- 可缓存
因为纯函数的相同输入始终有相同的结果,所以可以把纯函数的结果缓存起来
// 例如 :求圆的面积
const _ = require('lodash')
function getArea(r){
console.log(r)
return Math.PI * r * r
}
let getAreaWithMemory = _.memoize(getArea);
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
// 会打印一次4 即为传入参数,且其他数值圆的面积数值为缓存的数值。
// 模拟实现memoize
function memoize(f){
const cache = {}
return function(){
let key = JSON.stringify(arguments);
cache[key] = cache[key] || f.apply(f,arguments)
return cache[key]
}
}
let getAreaWithMemory = memoize(getArea);
console.log(getAreaWithMemory(4));
console.log(getAreaWithMemory(4));
console.log(getAreaWithMemory(4));
- 复用和组合能力强
- 可测试
测试就是已知输入值对输出值进行预判,如果结果不如预判那么默认为代码逻辑问题。纯函数可以让测试更加直观和快速方便。 - 可并行
多线程环境操作共享的内存数据很可能会出现意外情况
纯函数不需要访问共享的内存数据所以在并行环境下可以运行并不会影响运行结果。
概念:2. 没有任何可观察的副作用副作用
副作用会让函数变为不是纯函数了
// 不纯的
let min = 18;
function checkAge(age){
return age>= min
}
// 纯的 (有硬编码)
function checkAge2(age){
let min = 18;
return age>=min
}
副作用的来源:
- 配置文件
- 数据库
- 获取用户的输入
所有的外部交互都有可能产生副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序带来不确定性和安全因隐患有可能会受到xss安全攻击。但是副租用不可能完全禁止,我们要尽量让其可控。
lodash lodash官网 是一个纯函数的功能库,提供了对数字,数组,对象和字符串以及函数等操作的方法
// 演示lodash
// first / last / toUpper / reverse / each / includes / find / findIndex
const _ = require('lodash')
const array = ['jack','tom','kate','lucy']
// console.log(_.first(array));
// console.log(_.last(array));
// console.log(_.toUpper(_.first(array)));
// console.log(_.reverse(array));
const r = _.each(array,(item,index)=>{
console.log(item,index);
})
console.log(r);
4. 柯里化
柯里化概念:Currying,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
// 柯里化解决硬编码问题
function checkAge(min) {
return function (age) {
age >= min
}
}
let checkAge = min => {
age => {
age >= min
}
}
let checkAgea18 = checkAge(18)
let checkAgea20 = checkAge(18)
console.log(checkAgea18(16))
console.log(checkAgea18(20))
lodash中的柯里化
_.curry(fn)
功能:创建一个函数,该函数接收一个或者多个fn的参数,如果sn所需要的的参数都被提供了则执行fn函数,并返回结果,否则继续返回该函数并等待接收剩余参数。
参数 : 需要柯里化的函数
返回值: 柯里化后的函数
// lodash中curry的使用
const _ = require('lodash')
function getSum(a,b,c){
return a+b+c;
}
const curried = _.curry(getSum)
console.log(curried(1,2,3));
console.log(curried(1)(2,3));
console.log(curried(1,2)(3));
// 柯里化案例
const match = _.curry(function(reg,str){
return str.match(reg)
})
const haveSpace = match(/\s+/g)
const haveNumber = match(/\d+/g)
const filter = _.curry((func,array)=>{
return array.filter(func)
})
const findSpace = filter(haveSpace)
// console.log(haveSpace('hello world'));
// console.log(haveNumber('abc123'));
console.log(filter(haveSpace,['John Connor','John_Donne']));
console.log(findSpace(['John Connor','John_Donne']));
- 模拟_.curry()的实现
function getSum(a,b,c){
return a+b+c;
}
const curried = curry(getSum)
console.log(curried(1,2,3));
console.log(curried(1)(2,3));
console.log(curried(1,2)(3));
function curry(func){
return function curriedFn(...args){
if(args.length<func.length){
return function(){
return curriedFn(...args.concat(Array.from(arguments)))
}
}
return func(...args)
}
}
总结:
- 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住某些固定参数的新
- 简单来说就是让每个函数的参数单一化,颗粒度更小,更容易复用,就像乐高,功能单一,可以组装在任意的模型里面。
- 柯里化内部就是闭包 ,即为对参数的缓存
- 灵活 让函数颗粒度更小
- 可以把多元化的函数转化为一元话的函数
5. 函数组合
- 纯函数和柯里化很容易写出洋葱代码 h(g(f(x())))
- 获取数组的最后一个元素在转换成大写字母 toUpper(.first(_.reverse(array)))
- 函数组合可以让我们把细粒度的函数重新组合生成一个新的函数
管道
- 一个参数进入一个函数 返回一个结果这个函数就可以称为管道,当中间的函数很复杂的时候可以把它进行拆分。
参数进入管道后经过多个函数返回最终结果,管道拆分,就是函数拆分,如果有问题更容易发。
函数组合
- 概念 compose :一个函数经过多个函数的处理才能得到最终值,这时候可以把中间的过程的函数合并成一个函数
- 函数就想是数据的管道,函数组合就是连接这些管道,让数据穿过中间管道(函数)生成最终的结果
- 函数组合默认从右到左
// 函数组合
function compose(f,g){
return function(value){
return f(g(value))
}
}
function reverse(arr){
return arr.reverse();
}
function first(arr){
return arr[0]
}
let last = compose(first,reverse)
console.log(last([1,2,3,4]));
- lodash中的组合函数 flow()或flowRight()
// lodash中函数组合 _.flowRight
const _ = require('lodash')
const reverse = arr => arr.reverse();
const first = arr => arr[0]
const toUpper = s => s.toUpperCase();
const f = _.flowRight(toUpper,first,reverse)
console.log(f(['one','two','three']));
- 模拟实现lodash的flowRight方法
// 模拟实现lodash的flowRight方法
const reverse = arr=>arr.reverse();
const first = arr=>arr[0]
const toUpper = s=>s.toUpperCase();
function compose(...args){
return function(value){
return args.reverse().reduce((acc,fn)=>{
return fn(acc)
},value)
}
}
const compose = (...args)=>value=>args.reverse().reduce((acc,fn)=>fn(acc),value)
const f = compose(toUpper,first,reverse)
console.log(f(['one','two','three']));
函数的组合要满足结合律,组合不一致返回结果是一样的
const _ = require('lodash')
const f = _.flowRight(_.toUpper,_.first,_.reverse)
const f1 = _.flowRight(_.flowRight(_.toUpper,_.first),_.reverse)
const f2 = _.flowRight(_.toUpper,_.flowRight(_.first,_.reverse))
console.log(f(['one','two','three']));
console.log(f1(['one','two','three']));
console.log(f2(['one','two','three']));
组合调试
const _ = require('lodash')
const trace = _.curry((tag,v)=>{
console.log(tag,v);
return v
})
const split = _.curry((sep,str)=>_.split(str,sep))
const jion = _.curry((sep,arr)=>_.join(arr,sep))
const map = _.curry((fn,arr)=>_.map(arr,fn))
const f = _.flowRight(jion('-'),trace('map之后'),map(_.toLower),trace('split之后'),split(' '))
console.log(f('never say die'));
- lodash/fp ,lodash的fp模块提供了实用的对函数式编程友好的方法,且都为纯函数
const fp = require('lodash/fp')
const _ = require('lodash')
const f = fp.flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' '))
console.log(f('NEVER SAY DIE'));
console.log(_.map(['23','8','10'],parseInt));
console.log(fp.map(parseInt,['23','8','10']));
6. Point Free
我们把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的参数,只是把运算步骤合成到一起,使用这种模式需要定义基本运算函数
- 不需要指明处理的函数
- 只合成运算过程
- 需要定义辅助运算函数
const f = fp.flowRight(fp.join(’-’),fp.map(_.toLower),fp.split(’ '))
// 非 Point Free模式
// function f(word){
// return word.toLowerCase().replace(/\s+/g,'_')
// }
// console.log(f('Hello World'));
// Point Free
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g,'_'),fp.toLower)
console.log(f('Hello World'));
// 实用Point Free,模式 将单词中的首字母提取出来,并转换为大些
const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))
console.log(firstLetterToUpper('world wild web'));