新书终于要来了。我今天有一点空闲时间。我将专门写一篇关于JavaScript语言风格的文章。主角是函数式声明式风格。灵活的JavaScript及其多范式相信“功能性”这个概念对于很多前端开发者来说已经不再陌生:我们知道JavaScript是一门非常灵活的集成了多范式(multiparadigm)的语言,本文将展示JavaScript中的命令切换声明式语言风格和声明式风格的目的是让读者了解这两种不同语言模式各自的特点,进而在日常开发中做出合理的选择,充分发挥JavaScript的最大威力。为了讲解方便,我们从一个典型的事件发布订阅系统入手,逐步完成功能风格的改造。事件发布订阅系统,即所谓的观察者模式(Pub/Submode),秉承事件驱动(event-driven)的思想,实现了“高内聚低耦合”的设计。如果读者对这种模式不熟悉,建议先阅读我的原创文章:探索Node.js事件机制源码,打造属于你自己的事件发布订阅系统。本文从Node.js的源码入手,分析事件发布订阅系统的实现,实现了一种基于ESNext语法的命令式事件发布模式。对于这个基本内容,本文不会过多展开。典型的EventEmitter和转换挑战为了理解事件发布和订阅系统实现的思想,让我们看一个简单典型的基本实现:classEventManager{construct(eventMap=newMap()){this.eventMap=eventMap;}addEventListener(event,handler){if(this.eventMap.has(event)){this.eventMap.set(event,this.eventMap.get(event).concat([handler]));}else{this.eventMap.set(event,[handler]);}}dispatchEvent(event){if(this.eventMap.has(event)){consthandlers=this.eventMap.get(event);for(constiinhandlers){handlers[i]();}}}}上面的代码实现了一个EventManager类:我们维护一个Map类型的eventMap来维护不同事件的所有回调函数(handlers)。addEventListener方法存储指定事件的回调函数;dispatchEvent方法对指定的触发事件一一执行回调函数。在消费者层面:constem=newEventManager();em.addEventListner('你好',function(){console.log('hi');});em.dispatchEvent('你好');//你好,这些更容易理解。我们的下一个挑战是:将上面20行命令式代码转换为7行带有2个表达式的声明式代码;不再使用{...}和if判断条件;使用纯函数实现来避免副作用;使用一元函数,即函数方程中只需要一个参数;使函数可组合;代码实现应该干净、优雅、低耦合。Step1:使用函数代替类基于以上挑战,addEventListener和dispatchEvent不再作为EventManager类的方法出现,而是成为两个独立的函数,eventMap作为变量使用:consteventMap=newMap();functionaddEventListener(event,handler){if(eventMap.has(event)){eventMap.set(event,eventMap.get(event).concat([handler]));}else{eventMap.set(event,[handler]);}}functiondispatchEvent(event){if(eventMap.has(event)){consthandlers=this.eventMap.get(event);for(constiinhandlers){handlers[i]();}}}在模块化需求下,我们可以导出这两个函数:exportdefault{addEventListener,dispatchEvent};同时使用import引入依赖。注意import使用单例模式(singleton):import*asEMfrom'./event-manager.js';EM.dispatchEvent('event');因为模块是单例,不同文件导入时,内部变量eventMap是共享的,完全符合预期。Step2:使用箭头函数箭头函数不同于传统的函数表达式,更符合函数式的“口味”:consteventMap=newMap();constaddEventListener=(event,handler)=>{if(eventMap.has(event)){eventMap.set(event,eventMap.get(event).concat([handler]));}else{eventMap.set(event,[handler]);}}constdispatchEvent=event=>{if(eventMap.has(event)){consthandlers=eventMap.get(event);for(constiinhandlers){handlers[i]();}}}这里要特别注意箭头函数对this的绑定。Step3:去除副作用,增加返回值,保证纯函数特性。与上面的处理不同的是,我们不能再改变eventMap,而是应该返回一个新的Map类型变量,同时改变addEventListener和dispatchEvent方法的参数,添加“最后状态”的eventMap,以便导??出一个neweventMap:constaddEventListener=(event,handler,eventMap)=>{if(eventMap.has(event)){returnnewMap(eventMap).set(event,eventMap.get(event).concat([处理程序]));}else{returnnewMap(eventMap).set(event,[handler]);}}constdispatchEvent=(event,eventMap)=>{if(eventMap.has(event)){consthandlers=eventMap.get(event);for(constiinhandlers){handlers[i]();}}returneventMap;}是的,这个过程很像redux中reducer的功能类似。保持功能纯净是功能哲学中极其重要的一点。第4步:删除声明式for循环接下来,我们使用forEach代替for循环:constaddEventListener=(event,handler,eventMap)=>{if(eventMap.has(event)){returnnewMap(eventMap).set(事件,eventMap.get(event).concat([handler]));}else{returnnewMap(eventMap).set(event,[handler]);}}constdispatchEvent=(event,eventMap)=>{if(eventMap.has(event)){eventMap.get(event).forEach(a=>a());}returneventMap;}Step5:应用二元运算符我们使用||和&&让代码更具功能性.concat([处理程序]));}else{returnnewMap(eventMap).set(event,[handler]);}}constdispatchEvent=(event,eventMap)=>{return(eventMap.has(event)&&eventMap.get(event).forEach(a=>a()))||}event;}需要特别注意return语句的表达方式:return(eventMap.has(event)&&eventMap.get(event).forEach(a=>a()))||event;Step6:用三元运算符代替if三元运算符更直观简洁:constaddEventListener=(event,handler,eventMap)=>{returneventMap.has(event)?newMap(eventMap).set(event,eventMap.get(event).concat([handler])):newMap(eventMap).set(event,[handler]);}constdispatchEvent=(event,eventMap)=复制代码>{返回(eventMap.has(event)&&eventMap.get(event).forEach(a=>a()))||event;}Step7:去掉大括号{...}因为箭头函数总是返回表达式的值,我们不需要任何{...}:constaddEventListener=(event,handler,eventMap)=>eventMap.有(事件)?newMap(eventMap).set(event,eventMap.get(event).concat([handler])):newMap(eventMap).set(event,[handler]);constdispatchEvent=(event,eventMap)=>(eventMap.has(event)&&eventMap.get(event).forEach(a=>a()))||event;Step8:完成柯里化的最后一步是实现柯里化操作。具体思路是将我们的函数改成一元的(只接受一个参数),实现方式是使用高阶函数(higher-orderfunction)为了简化理解,读者可以把参数(a,b,c)简单的改成a=>b=>c:constaddEventListener=handler=>event=>eventMap=>eventMap.has(event)?newMap(eventMap).set(event,eventMap.get(event).concat([handler])):newMap(eventMap).set(event,[handler]);constdispatchEvent=event=>eventMap=>(eventMap.has(event)&&eventMap.get(event).forEach(a=>a()))||事件;如果读者对此理解有些困难,建议先补充柯里化知识,这里不再展开。当然,这种处理需要考虑参数的顺序。让我们通过例子来消化。柯里化用法:constlog=x=>console.log(x)||x;constmyEventMap1=addEventListener(()=>log('hi'))('hello')(newMap());dispatchEvent('hello')(myEventMap1);//部分使用:constlog=x=>console.log(x)||x;让myEventMap2=newMap();constonHello=handler=>myEventMap2=addEventListener(handler)('hello')(myEventMap2);consthello=()=>dispatchEvent('hello')(myEventMap2);onHello(()=>log('hi'));你好();//hi熟悉python的读者可能会更好地理解partial的概念。简单来说,函数的偏应用可以理解为:当函数执行时,必须带上所有必要的参数来调用它。但是,有时可以在调用函数之前提前知道参数。在这种情况下,一个函数有一个或多个预先可用的参数,以便可以用更少的参数调用该函数。对于onHello函数,它的参数是触发hello事件时的回调。这里myEventMap2和hello事件是预先设置的。hello函数也是一样,只需要触发hello事件即可。结合使用:constlog=x=>console.log(x)||X;constcompose=(...fns)=>fns.reduce((f,g)=>(...args)=>f(g(...args)));constaddEventListeners=compose(log,addEventListener(()=>log('hey'))('hello'),addEventListener(()=>log('hi'))('hello'));constmyEventMap3=addEventListeners(新地图());//myEventMap3dispatchEvent('你好')(myEventMap3);//嘿嘿这里你需要特别注意compose方法。熟悉Redux的读者如果看过Redux的源码,一定对compose不陌生。通过compose,我们实现了hello事件的两个回调函数的组合和log函数的组合。关于compose方法的奥秘和不同的实现方式,请关注作者:LucasHC。我会专门写一篇文章来介绍,分析为什么Redux对compose的实现有点晦涩难懂,分析一个更直观的实现。综上所述,函数式概念对初学者来说可能不是很友好。读者可以根据自己的熟悉程度和喜好,在以上8个步骤中随时停止阅读。也欢迎讨论。本文为MartinNovák新文的释义,欢迎指正。广告时间:如果你对前端开发感兴趣,尤其是React技术栈:我的新书,或许有你想看的。关注作者LucasHC,新书出版时有赠书。快乐编码!PS:作者Github仓库和知乎问答链接,欢迎各种形式的交流。
