函数式编程
什么是函数式编程
先了解一下编程范式,编程范式是指计算机编程的的基本风格或典范模式,我的理解是编程范式是解决问题的不同方法,从不同的角度,不同层面去解决问题,这里的问题是指达到想要的效果。
比如面向对象编程、面向过程编程、函数式编程,这些都是编程范式,但是解决问题的思维方式是不同的
面向对象编程:将现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承、多态演示事物间的联系;
面向过程编程:关注问题解决的具体过程,将解决方法一步一步的列出来,按照步骤可以解决问题;
函数式编程:把现实世界中事物之间的联系抽象到程序世界,即将运算过程抽象出来;
- 程序的本质:输入通过某种运算获得相应的输出
- 函数式编程中函数的意思指的是输入与输出在数学中的关系、联系,函数式编程就是用来描述数据之间的映射
- 相同的输入总会得到相同的输出(纯函数)
前置知识
- 函数是一等公民 函数可以存储在变量中,函数可以作为参数、函数可以作为返回值
- 高阶函数(High Order Function) 将函数当做参数或将函数作为返回值的函数
//函数作为参数
function forEach(array, fn){
for(let value of array){
return fn(value)
}
}
//函数作为返回值
function makeMsg(){
let msg = 'Hello Msg';
return function(){
console.log(msg);
}
}
高阶函数可以用来抽象通用的问题,不再重复写一些描述过程的代码,专注于目标,屏蔽掉实现的过程跟细节
- 闭包(closure)
- 函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包,可以在另一个作用域内调用一个函数的内部函数并访问到该函数的作用域中的成员
- 本质:函数在执行的时候会放到一个执行栈上,执行完毕后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,所以内部函数依然能够访问到外部函数的成员
//闭包案例
//计算m的n次幂
function makePower(power){
return function(number){
return Math.pow(number,power)
}
}
//2次幂
let makePower2 = makePower(2)
//3次幂
let makePower3 = makePower(3)
//计算4的2次幂跟3次幂
makePower2(4)
makePower3(4)
纯函数
纯函数概念
相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
纯函数类似数学中的函数,用来表示输入与输出的映射关系
纯函数功能库:lodash,提供了对数组、数字、对象、字符串、函数的操作方法
纯函数的好处
- 可缓存 纯函数对相同的输入始终有相同的输出,所以可以将具有相同输入的纯函数的结果缓存起来,对一些计算量或者计算难度很大的计算缓存结果
//模拟loadsh中的memoize函数,该函数可以在一个函数调用一次过后,对其结果进行缓存,再调用时直接返回输出值
function memoize(f){
let cache = {};
return function(){
let arg_str = JSON.stringify(arguments);
cache[arg_str] = cache[arg_str] || f.apply(f, arguments)
return cache[arg_str]
}
}
//定义一个cache对象用来对输出值进行缓存,属性可以设为f的参数,字符串化的arguments,然后将函数运行结果赋给cache[arg_str],这样在下次运算时,会首先检查cache对象中有没有缓存结果,如果有,直接返回,如果没有,再调用f
- 可测试 纯函数测试更加方便
- 并行处理 纯函数只依赖输入,一般不会使用共享的内存数据,不会出现因使用共享数据而出现的冲突
副作用
副作用会使一个纯函数变得不纯,副作用的来源包括配置文件、数据库、来自用户的输入、、、
//函数makeAge中用到的min变量需要从外部获取,当min发生改变时,相同的输入会导致不相同的输出,使纯函数变得不纯
let min = 18;
function makeAge(age){
return age > min
}
//一个解决方法,将min定义在函数内部
function makeAge(age){
let age = 18;
return age > min;
}
//这种方式可以避免纯函数变得不纯,但是存在硬编码let min = 18,后续可以通过柯里化解决
柯里化(Haskell Brooks Curry)
使用柯里化解决上面案例中的硬编码问题
//普通的纯函数
function checkAge(min, age){
return age > min
}
//柯里化
function checkAge(min){
return function (age){
return age > min
}
}
//es6写法
let checkAge = min =>(age => age > min)
let checkAge18 = checkAge(18);
let checkAge20 = checkAge(20);
checkAge18(20);
checkAge20(24);
柯里化:
- 当一个函数有多个参数时,先传递给函数一部分参数调用它(这部分参数永远不变)
- 然后返回一个新的函数接收剩余的参数,返回结果
//几个使用lodash纯函数库的柯里化函数
const _ = require('lodash')
//判断字符串是否匹配的柯里化函数
const match = _.curry(function(reg, str){
return str.match(reg)
})
const haveSpace = match('/\s+/');
const haveNumber = match('/\d+/');
console.log(haveSpace('hello world'));
console.log(haveNumber('24%'));
//filter的柯里化函数
const filter = _.curry(function(func, array){
return array.filter(func)
})
const findSpace = filter(haveSpace);
console.log(findSpce(['anne haisewell', 'john_dane']));
//模拟_.curry()实现
const curry = function (func){
return curried(...args){
//判断传入的实参args与形参是否相等,如果小于,则返回一个函数再接收剩余参数
if(args.length < func.length){
return function(){
return curried(...args.concat(Array.from(arguments)))
}
}
//若实参个数大于等于形参,调用func函数
return func(...args);
}
}
总结
- 柯里化可以给一个函数传递较少的参数,并生成一个已经记住了某些固定参数的新函数来接收剩余参数;
- 这是一种对函数参数的缓存;
- 可以让函数变得更灵活,让函数的粒度更小(我理解为每次传入的参数更少);
- 可以将多元函数转化为一元函数,可以对函数进行组合,产生更强大的功能;