函数式编程命令式和声明式我们在开始编程的时候,通常都是从命令式编程开始,最简单的过程式代码。后来为了应对大型项目,接触到了面向对象,也是势在必行。函数式编程是声明式的,它其实比面向对象出现得更早。MySQL是声明式语言的一个很好的例子。它只是声明了过程而没有暴露过程的细节。SELECT*fromdata_baseWHEREauthor='jack';再举个例子来比较命令式和声明式代码:constarr=[1,2,3,4,5]//要求上面的数组小于3的去掉,剩余数乘以2//命令式constresult=[]for(leti=0;i=3){result.push(arr[i]*2)}}//声明式constresult=arr.filter(n>=3).map(n=>n*2)看完上面的例子,你可能会想,声明式代码就是这样?再看看上面的MySQL例子。声明式风格确实如此。通过filter、map等方法封装细节,然后声明逻辑。它不需要解释如何一步一步地做,所以更简洁。而这是最基本的声明性代码。我们在学习面向对象的时候,首先会知道它的三个特点:封装、继承、多态。基于这三个特点,人们在编写代码时会延伸出很多最佳实践,即设计模式,如工厂模式、依赖注入等。函数式编程也是如此,具有相应的特点和最佳实践。函数式编程是一种基于函数的编程风格。其实程序是由一段段逻辑组成的,而这些逻辑又被分解成函数,然后组合到极致。函数式风格解决了一个问题。当用命令式风格写代码时,一开始可以直接完成任务代码,但是当你开始考虑边界处理和代码重用时,渐渐地,你的代码就会逐渐承担起自己不必要的复杂性,而函数式编程可以解决这个问题.一个程序的本质,除了数据结构和算法,还可以是计算和副作用。//先看一个例子,从localStorage中取出所有用户数据,找出最旧的,显示在DOM上constusers=localStorage.getItem('users')constsortedUsers=JSON.parse(users).sort((a,b)=>b.age-a.age)constoldestUser=sortedUsers[0]document.querySelector('#user').innerText=oldestUser.name上面的代码很常见,但是需要一些时间理解,先做一个功能优化,把上面的步骤全部封装起来。constgetLocalStorage=(key)=>localStorage.getItem(key);constgetSortedUser=users=>users.sort((a,b)=>b.age-a.age)constfirst=arr=>arr[0]constwriteDom=selector=text=>文档。查询选择器(选择器)。innerText=文本;constprop=key=obj=>obj[key]writeDom('#user')(prop('name')(first(getSortedUser(JSON.parse(getLocalStorage('user'))))))我知道上面的代码看起来很奇怪,一是最终的调用逻辑需要从右往左看,二是writeDom和prop多次传入参数,也就是柯里化。尽管看起来很奇怪,但逻辑比以前更清晰易读,而且所有功能都非常灵活和可扩展。这是函数式代码,强调声明式代码和函数的灵活组合。函数式编程的目标是使用函数来抽象控制流和对数据的操作,从而消除副作用并减少系统中的状态变化。这将在后面更详细地解释。下面进一步优化代码。柯里化柯里化就是将一个一次传入多个参数的函数,转化为一个可以批量传入参数的函数。但是现在要讨论参数的顺序,比如柯里化和封装Array.map,不同的参数顺序会导致不同的效果:constlistMap=fn=>list=>list.map(fn)constallAddOne=listMap((o)=>o+1)allAddOne([1,2])//[2,3]//如果颠倒过来就是下面这样constlistMap2=list=>fn=>list.map(fn)constmapList=listMap2([1,2])mapList(o=>o+2)//[2,3]前者先传fn再传list,更符合functional规则,因为functional是组合逻辑,而不是数据组合。这也解释了为什么前面的例子需要柯里化一些函数,组合起来更方便。优化后的代码——仿函数(functor)其实我之前写过如下声明式代码://[1,2]=>[1,2,3]=>[2,3]=>[6,9][1,2].concat([3]).filter(x=>x>1).map(x=>x*3)之前写的时候觉得这个链式调用的写法很清晰,如果所有的逻辑都可以这样写就好了,函数式编程就是这样的思路。如果我们要处理非数组,其实可以用数组包裹起来,比如//['123']=>['123']=>[123]=[124]?124['123']。map(o=>o.trim()).map(o=>Number(o)).map(o=>o+1).pop()//124但是上面的并不优雅,每次都要包裹在一个数组中。所以我们可以创建一个包含map方法的对象,从而链接调用:constBox=(v)=>{return{map(fn){//将数据放入框中returnBox(fn(v))},getValue(){//从框中获取数据returnv}}}//重写上面的例子Box('123').map(o=>o.trim()).map(o=>Number(o)).map(o=>o+1).getValue()//124//重写前面的例子Box(getLocalStorage('user')).map(JOSN.parse).map((o)=>o.sort((a,b)=>b.age-a.age)).map(first).map(prop('name')).map(writeDom('#user'))//比较之前的长列表看起来干净多了,这个带有map方法的Box在函数式编程中叫做functor,是一个包装数据的容器,提供链接数据操作的能力。进一步实践——Maybe但是上面的例子还是有一个问题,就是getLocalStorage可能没有返回有效数据,也可能是undefined,会导致后续报错。因此,当目标不存在时,最好跳过所有后续操作。功能思路是我们写两个box,一个叫Just,会正常处理所有流程,另一个叫Nothing,会自动跳过所有流程,我们只需要一开始判断使用哪个Box即可。//有值constJust=(val)=>({map:(f)=>Just(f(val)),getValue:()=>val,isJust:()=>true})//没有valueconstNothing=()=>({map:(f)=>Nothing(),//Nothing不会执行所有后续操作getValue:(defaultVal)=>defaultVal,isJust:()=>false})//然后用一个Maybe判断两个特殊的Boxes用哪个constMaybe=(val)=>val===null||值===未定义?Nothing():Just(val)//ReuseMaybe写前面的例子Maybe(getLocalStorage('user')).map(JOSN.parse).map((o)=>o.sort((a,b)=>b.age-a.age)).map(first).map(prop('name')).map(writeDom('#user'))上面的Maybe也叫Monad,是基于functor实现的(盒子)。可以这样理解,Monad是专门处理某些场景的functor,还有很多类似的Monad,用来处理函数式风格遇到的各种场景。另一个常见的monad会在后面介绍。纯函数和副作用事实上,React中的每一个渲染函数都是一个纯函数,相当于UI=Function(state)。所以每次修改状态时,React都会重新运行渲染函数。只要传入同一个参数,无论调用多少次,渲染出来的UI都是一致的。这是一个纯函数。纯函数有什么优点?1.无副作用,可任意放置,不影响上下文,易于组合;2.易于维护和重构,只要输入输出一致,随意改动不会影响外部;3、输出稳定,易于单元测试;4.输入输出输出完全对应,方便缓存,只需要判断输入是否发生变化即可。但是纯函数的另一面就是副作用,因为当前程序在运行的时候必须要进行IO操作(请求,DOM操作等)。这些就是所谓的副作用。如果一个函数包含副作用,它就不能被多次调用。结果是一致的,可能无法指定哪个内部变量被副作用修改了。React处理副作用的方式是使用useEffect来收集副作用。副作用虽然不能完全去除,但是可以收集起来统一控制。这也是为什么useEffect中的函数不是在渲染函数执行的过程中执行,而是维护在一个队列中,渲染完再执行,目的是为了统一处理副作用,保持渲染函数的纯净。那么在一般的函数式编程中,如何处理副作用呢?Sideeffects-IOMonad与React的思路相同,专注于处理。将副作用包装在一个仿函数中并添加一个runIO方法的好处是它只会在调用最终的runIO方法时执行,因此可以安全地处理副作用。constIO=(fn)=>{return{map(fn){returnIO(()=>fn(sideEffectFn()))//延迟所有副作用的执行},runIO(){//显式调用IO所以那我们可以把IO操作更清晰的放在一起,方便管理fn()},}}//Example:constgetNumDom=()=>document.querySelector('#num')constwriteNum=(text)=>{document.querySelector('#num').innerText=tex}constioEffect=Maybe(getLocalStorage('user')).map(JOSN.parse).map((o)=>o.sort((a,b)=>b.age-a.age)).map(first).map(prop('name')).map(o=>IO(()=>writeDom('#user')(o)))ioEffect.getValue().runIO()//一次性运行副作用除了集中控制副作用外,还可以将读数据、处理数据、写数据清晰分离,更有利于读取和维护。但是上面的return是Maybe(IO(o)),Monad是嵌套的,导致最终的调用是多余的:ioEffect.getValue().runIO()。这个时候我们只需要给Maybe添加一个foldMap方法就可以了。该方法的目的是在传入下一个Monad时触摸嵌套。...foldMap(monad){returnmonad(val)}...//重写上面的嵌套示例constioEffect=Maybe(getLocalStorage('user')).map(JOSN.parse).map((o)=>o.sort((a,b)=>b.age-a.age)).map(first).map(prop('name')).foldMap(o=>IO(()=>writeDom('#user')(o)))ioEffect.runIO()可以通过foldMap解决Monad的嵌套问题,所以foldMap是Monad的必备方法。组合函数,让数据流更简洁Box.map(fn).map(fn)虽然看起来很清楚,其实还有另一种写函数的方式,就是写一个不带map的函数组合所有函数的方法。看起来更简洁。尝试编写此方法,将其称为管道,像水管一样组合函数:constpipe=(...fns)=>(arg)=>fns.reduce((lastVal,fn)=>fn(lastVal),arg)在为了将Monad放入管道,我们需要封装map和foldMap的两个必要方法:constmap=fn=>monad=>monad.map(fn)constfoldMap=fn=>monad=>monad.foldMap(fn)//重写前面的例子constioEffect=pipe(getLocalStorage,Maybe,map((o)=>o.sort((a,b)=>b.age-a.age))map(first)map(prop('name'))foldMap(o=>IO(()=>writeDom('#user')(o))))('user')ioEffect.runIO()现在利用成熟的库两个相对成熟的JavaScript工具库market-lodash/fp和Ramda,其中包含的工具函数已经柯里化,默认先传递函数再传递处理后的数据。Currying结合上面的pipe方法可以这样写:constarr=[{n:'5'},{n:'12'}]//日常写arr.map(o=>o.n).map(o=>Number(o)).filter(o=>o<10)//=>[5]//withlodash/fpimport{map,filter}from'lodash/fp'pipe(map('n'),Number,filter(o=>o<10))(arr)也叫pointfree,只考虑函数的组合,不需要考虑参数什么时候传入,因为最终它们会形成一个pipeline,一端输入参数,另一端自然出结果,中间过程可以任意替换。参考:https://llh911001.gitbooks.io...https://book.douban.com/subje...https://egghead.io/lessons/ja...https://www.ruanyifeng。com/bl...