当前位置: 首页 > Web前端 > JavaScript

看一篇文章了解JavaScript函数式编程的初识

时间:2023-03-27 15:20:25 JavaScript

背景???????????????????????????????????????????函数式编程可以说是一种非常古老的编程方式,但是近几年却成为了一个非常热门的话题。无论是Google推动的Go,学术界的Scala和Haskell,还是Lisp的新语言Clojure,这些新的函数式编程语言越来越受到关注。函数式编程的思想对前端影响很大,Angular、React、Vue等流行的框架一直在使用这种思想来解决问题。作为一种高级编程范式,函数式编程更接近于数学和代数的编程范式。完全不同于面向对象的开发理念和思维方式。深刻理解这种差异,是程序员进阶之路的必经之路。ProgrammingParadigm编程范式(ProgrammingParadigm)是编程语言领域的一种范式风格,它反映了开发人员在设计编程语言时的考虑,也影响着使用相应语言的程序员的编程风格。大致可以分为两大类,具体内容如下图所示:函数式概念与思考函数式编程是一种基于Lambda演算的语言模型,它的实现是基于Lambda演算和更具体的α-等价,β-减少和其他设置。这是比较官方的解释。不要被这个概念吓倒。很可能你在日常开发中用到了很多函数式编程的概念和工具。比如越来越函数化的ES6,在新规范中加入了很多新特性,其中很多都是借鉴了其他函数式语言的特性,为JavaScript语言增加了很多新的函数式特性。箭头函数是ES6发布的新特性。箭头函数也叫胖箭头(FatArrow),大致是从CoffeeScript或Scala语言借来的。箭头函数是提供词法作用域的匿名函数。函数式编程思想的目标:在程序执行时,应尽量减少程序对结果以外的数据的影响。函数式编程的特点声明式(Declarative)纯函数(PureFunction)函数的执行过程完全由输入的参数决定,不会受到参数以外的任何数据的影响。该函数不修改任何外部状态,例如修改全局变量或传入参数对象。数据不变性(Immutability)当我们需要改变数据的状态时,保持原有数据不变,并生成一个新的数据来反映这种变化。不可变数据是不可变数据。一旦生成,可以确定它的值永远不会改变,这对代码理解很有帮助。下面是解释命令式编程和函数式编程的对比代码://计算传入的数据并乘以2//命令式编程functiondouble(arr){constresults=[]for(leti=0;i项目*2);}constoneArray=[1,2,3];constanotherArray=double(oneArray);安慰。日志(一个数组);//[1,2,3]console.log(anotherArray);//[2,4,6]函数是一等公民数字在JavaScript中是一等公民,同时也是一等公民的函数将具有与数字相似的属性。函数,如数字,可以存储为变量letone=function(){return1};函数,如数字,可以存储为数组的一个元素letones=[1,function(){return1}];函数,如数字,可以传递给另一个函数functionnumAdd(n,f){returnn+f()};numAdd(1,function(){return1});//2个函数可以像数字一样被另一个函数返回return1;returnfunction(){return1};最后两点其实是“高阶”函数的定义;高阶函数应该能够执行至少一项,将函数作为参数或返回函数作为结果。高阶函数(HighOrderFunction)高阶函数,通俗的说就是以其他函数为参数,返回其他函数的函数。我们称函数的嵌套高阶调用为高阶函数。高阶函数可以说是编程语言方便实践函数式风格的基础。比如我们在React中会遇到的HOC。添加千位符号的代码如下:constaddThousandSeprator=(strOrNum)=>{returnparseFloat(strOrNum).toString().split('.').map((x,idx)=>{if(!idx){returnx.split('').reverse().map((xx,idxx)=>(idxx&&!(idxx%3))?(xx+','):xx)verse.re().join('')}else{returnx;,会逐渐返回配置的函数,直到所有参数都用完functioncurry(fun){returnfunction(arg){returnfun(arg)}}constarr=['1','2','3','4'].map(curry(parseInt));console.log(arr)//[1,2,3,4]使用柯里化相对容易生成流畅的函数式API。在Haskell编程语言中,函数默认是柯里化的。但在JavaScript中,函数式API必须使用柯里化设计,并且必须记录在案。递归程序调用自身的编程技术称为递归。递归作为一种算法在编程语言中被广泛使用。递归是一种解决进程堆叠的方法,在运行时承担更多的工作。递归的力量在于用有限的语句定义对象的无限集合。通常,递归需要边界条件、递归前向段和递归返回段。当不满足边界条件时,递归前进;当满足边界条件时,递归返回。说到递归,就不得不说说尾递归。早期的浏览器引擎不支持尾递归,所以当我们计算经典斐波那契数列或进行其他递归操作时,可能会触发堆栈调用超出限制的提醒。如果每次递归结束返回的内容是一个待计算的表达式,那么等待计算的变量和环境总会被压入运行时内存栈,这就是溢出的根本原因。而如果我们使用新的递归方式,如果运行环境支持优化,被替换的函数负载会立即释放。//递归:将外部调用保存在内存栈中constfactorialFn=(n)=>{if(n<=1){return1;}else{返回n+factorialFn(n-1);}}安慰。log('factorialFn:',factorialFn(30))//返回函数调用;尾递归优化constfactorialFun=(n,acc)=>{if(n<=1){returnacc;}else{returnfactorialFun(n-1,n+acc)}}console.log('factorialFun:',factorialFun(30,1))结果如下:基于流的编程在前端领域,“流”的经典代表之一《RxJS》。在Rx官网https://reactivex.io/上,有一段介绍文字:AnAPIforasynchronousprogrammingwithobservablestreams。老实说,这个描述并没有把概念说清楚,所以下面我们就用通俗的语言来解释一下Rx。RxJS初识RxJS是ReactiveExtension模式的JavaScript语言实现。RxJS是一个使用可观察序列编写异步和基于事件的程序的库。它提供了一个核心类型,Observable,广播类型(Observer,Schedulers,Subjects)和操作符(map,filter,reduce等),允许异步事件作为集合处理。RxJS的运行是Observable和Observer之间的交互游戏。RxJS中的数据流是一个Observable对象。Observable实现了两种设计模式:ObserverPattern和IteratorPattern。Observable和Observer的关系是观察者模式和迭代器模式的结合。通过Observable对象的订阅功能,一个Observer对象可以订阅一个Observable对象的推送内容,而取消订阅的内容可以通过unsubscribe函数取消订阅。RxJS核心概念Observable:Observable对象,表示可以调用的未来值或事件集合的方法。Observer:Observer是一组回调函数,处理Observable提供的值。/***Observable对象(source$)是一个发布者。发布者和观察者通过Observable对象的订阅函数连接。*Console.log作为观察者。不管传入什么“事件”,它都只是将“事件”输出到控制台*/constsource$=of(1,2,3);//发布者source$.subscribe(console.log);//观察者代码输出如下:Subscription:订阅关系,表示Observable执行,主要用于取消执行。从'rxjs/Observable'导入{Observable};constonSubscribe=observer=>{让数字=1;consthandle=setInterval(()=>{console.log(`onSubscribe:${number}`)observer.next(number++);},1000);return{unsubscribe:()=>{clearInterval(handle);}};};constsource$=newObservable(onSubscribe);constsubscription=source$.subscribe(item=>console.log(`The${item}call`));setTimeout(()=>{subscription.unsubscribe();},5500);这段代码输出结果如下:clearInterval这行代码被注释掉(handle)后,代码输入结果如下:当unsubscribe函数中的clearInterval被注释掉,即setInterval没有被打断时,当前数在setInterval的函数参数中输出,修改后的程序会不断输出onSubscirbe:n。可以看出,Observable产生的事件只有通过subscribe订阅后才会被Observer接收到,unsubscribe后就接收不到了。Operators:运算符,纯函数,一个运算符是一个返回一个Observable对象的函数。说到算子,不得不说的就是弹球图了。弹球图可以通过动画非常直观的向我们展示操作过程,动态:https://reactive.how/rxjs/,静态:https://rxmarbles.com/#interval。在所有操作符中,map和filter可能是最容易理解的,因为JavaScript数组对象有两个同名的函数map和filter。JavaScript编写:constsource=[1,2,3,4,5,6];constresult=source.filer(x=>x%2===0).map(x=>x*2);console.log(结果);RxJS写法:constresult$=of(1,2,3,4,5,6).filter(x=>x%2===0).map(x=>x*2);result$.subscribe(控制台日志);按功能分类,大致可以分为9类:创建(creation)、转换(transformation)、过滤(filtering)、组合(combination)和多播(multicasting)错误处理类(errorHandling)辅助工作类(untility)条件分支类(conditional&boolean)数据和聚合类(mathematical&aggregate)Subject:主题,等同于EventEmitter,唯一的向多个Observers广播值或事件的方式。从'rxjs/Observable'导入{Observable};从'rxjs/Subject'导入{Subject};导入'rxjs/add/observable/interval';导入'rxjs/add/operator/take';consttick$=Observable.interval(1000).take(3);constsubject=newSubject();tick$.subscribe(subject);subject.subscribe(value=>console.log('observer1:'+value));setTimeout(()=>{subject.subscribe(value=>console.log('observer2:'+value));},1500);这段代码的执行结果如下:从上面的代码可以看出,Subject同时具有Observable和Observer的性质,就像有两张脸一样,可以是两面。浏览器鼠标移动事件和点击事件、浏览器滚动事件、WebSocket推送消息、Node.js支持的EventEmitter对象消息等日常常见场景,以及微服务系统中主应用和各个子应用的关系通信等。调度器:控制并发的集中式调度器,允许我们协调在setTimeout或其他时间发生的事件。Scheduler实例:undefined/null:即没有指定Scheduler,代表一个同步执行的Scheduler。尽快:调度程序尽快执行。async:使用setInterval实现的Scheduler,用于按时间吐出数据的场景。队列:使用队列实现的调度器,用于迭代大型集合。animationFrame:动画场景的调度器。RxJS默认选择Scheduler的原则是尽量减少并发操作。因此range选择undefined,指同步执行的Scheduler;对于大数据,选择队列;对于时间相关的运算符,例如间隔,选择异步。从'rxjs/Observable'导入{Observable};导入'rxjs/add/observable/range';从'rxjs/scheduler/asap'导入{asap};constsource$=Observable.range(1,3,asap);console.log('beforesubscribe');source$.subscribe(value=>console.log('data:',value),error=>控制台.log('error:',error),()=>console.log('complete'));console.log('aftersubscribe');这段代码的执行结果如下:前端风格?在开发web时,我们会在服务器端管理大量的系统状态和系统数据,可见随着前端工作流的逐渐增多,事件和远程状态响应会变得错综复杂。在查看一个超过10页或者复杂组件的项目代码时,我们会发现相比于后端,通过前端代码了解整个业务环节是比较困难的。如果我们用更合理的功能逻辑替换核心代码,或者使用功能性工具和规范来总结现有逻辑,我们可以显着提高代码的可读性和代码在运行时的可调试性。升级改造代码的方式之一。前端函数式风格的初衷是希望更好、更快、更强的解决开发过程中遇到的问题。与其等待后续治理,不如在日常开发中进行合理规划,养成良好的开发习惯。