当前位置: 首页 > 科技观察

这些高阶的函数技术,你掌握了么

时间:2023-03-19 21:19:33 科技观察

这些高阶功能技术你掌握了吗?转载本文请联系全栈修真之路公众号。在JavaScript中,函数是一等公民(FirstClass)。所谓“一等公民”,是指一个函数与其他数据类型处于平等地位,可以赋值给其他变量或作为参数传递给另一个函数,或作为另一个函数的返回值。接下来阿宝哥给大家介绍一些功能相关的技术。阅读本文后,您将了解高阶函数、函数组合、柯里化、偏函数、惰性函数、缓存函数的相关知识。1.高阶函数在数学和计算机科学中,高阶函数是至少满足以下条件之一的函数:接受一个或多个函数作为输入;输出一个函数。接收一个或多个函数作为输入,即函数作为参数传递。这个应用场景相信很多人都不陌生。比如常用的Array.prototype.map()和Array.prototype.filter()高阶函数://Array.prototype.map高阶函数constarray=[1,2,3,4];constmap=array.map(x=>x*2);//[2,4,6,8]//Array.prototype.filter高阶函数constwords=['semlinker','kakuqo','lolo','阿宝'];constresult=words.filter(word=>word.length>5);//["semlinker","kakuqo"]输出一个函数,即调用高阶函数后,返回一个新的函数。在我们日常工作中,常见的debounce和throttle函数都满足这个条件,所以也可以称为高阶函数。2.函数组合函数组合是将两个或多个函数组合起来生成一个新函数的过程:constcompose=function(f,g){returnfunction(x){returnf(g(x));};};在上面的代码中,f和g都是函数,x是一个参数,结合起来生成一个新函数。2.1功能组合的作用在项目开发过程中,为了实现功能的复用,我们通常会尽量保证功能的职责单一。例如,我们定义如下函数式函数:在上述函数式函数的基础上,我们可以自由组合函数来实现具体的功能:}functionupperCase(input){returninput&&typeofinput==="string"?input.toUpperCase():input;}functiontrim(input){returntypeofinput==="string"?input.trim():input;}functionsplit(input,delimiter=","){returntypeofinput==="string"?input.split(delimiter):input;}consttrimLowerCaseAndSplit=compose(trim,lowerCase,split);trimLowerCaseAndSplit("a,B,C");//["a","b","c"]在上面的代码中,我们通过compose函数实现了一个trimLowerCaseAndSplit函数。该函数首先对输入的字符串进行消隐处理,然后将字符串中包含的字母转为小写,最后使用分号对字符串进行分割。使用函数组合技术,我们可以很容易地实现一个trimUpperCaseAndSplit函数。2.2复合函数的实现Array.prototype.reduce方法用于实现复合函数的调度,对应的执行顺序是从左到右。这个执行顺序和Linux管道或者过滤器的执行顺序是一致的。但是如果你想从右向左执行,那么你可以使用Array.prototype.reduceRight方法来实现。其实每当看到compose函数,阿宝哥都会忍不住想起《如何更好地理解中间件和洋葱模型》一文介绍的compose函数:functioncompose(middleware){//省略部分代码returnfunction(context,next){letindex=-1;returndispatch(0);functiondispatch(i){if(i<=index)returnPromise.reject(newError("next()calledmultipletimes"));index=i;letfn=中间件[i];if(i===middleware.length)fn=next;if(!fn)returnPromise.resolve();try{returnPromise.resolve(fn(context,dispatch.bind(null,i+1)));}catch(err){returnPromise.reject(err);}}};}利用上面的compose函数,我们可以实现如下通用的任务处理流程:3.CurryingCurrying是一种处理函数的方法,它接受多个参数,并在其中使用这些函数只允许一个参数的框架。这种转换是一个现在称为“柯里化”的过程,在这个过程中我们可以将具有多个参数的函数转换为一系列嵌套函数。它返回一个新的函数,期望下一个传入的参数。当接收到足够的参数时,原始函数将自动执行。在理论计算机科学中,柯里化提供了简单的理论模型,例如在仅接受单个参数的lambda演算中研究具有多个参数的函数的方式。与柯里化相反的是非柯里化,一种使用匿名单参数函数实现多参数函数的方法。例如:constfunc=function(a){returnfunction(b){returna*a+b*b;}}func(3)(4);//25Uncurrying不是本文的重点,接下来我们使用curry函数由Lodash提供,直观感受“柯里化”函数后的变化:constabc=function(a,b,c){return[a,b,c];};constcurred=_.curry(abc);curried(1)(2)(3);//=>[1,2,3]curried(1,2)(3);//=>[1,2,3]curried(1,2,3);//=>[1,2,3]_.curry(func,[arity=func.length])创建一个函数,接收func的参数,或者调用func返回的结果,如果func需要的参数已经有了如果provided,直接返回func执行的结果。或者返回一个接受剩余func参数的函数,可以使用func.length来设置需要累加的参数个数。来源:https://www.lodashjs.com/docs/lodash.curry这里需要注意的是,数学和理论计算机科学中的柯里化函数一次只能传递一个参数。对于JavaScript语言,实际应用中的柯里化函数可以传递一个或多个参数。好了,介绍完柯里化的相关知识,我们来介绍一下柯里化的功能。3.1柯里化的作用3.1.1参数重用函数buildUri(scheme,domain,path){return`${scheme}://${domain}/${path}`;}constprofilePath=buildUri("https","github.com","semlinker/semlinker");constawesomeTsPath=buildUri("https","github.com","semlinker/awesome-typescript");在上面的代码中,首先我们定义了一个buildUri函数,该函数可以用来构建uri地址。然后我们使用buildUri函数构建阿宝哥的Github个人主页地址和awesome-typescript项目。对于上面的uri地址,我们发现https和github.com的两个参数值是一样的。如果需要继续构建阿宝哥其他项目的地址,需要重复设置相同的参数值。那么有什么办法可以简化这个过程呢?答案是肯定的,就是对buildUri函数进行curry处理。具体处理方法如下:const_=require("lodash");constbuildUriCurry=_.curry(buildUri);constmyGithubPath=buildUriCurry("https","github.com");constprofilePath=myGithubPath("semlinker/semlinker");constawesomeTsPath=myGithubPath("semlinker/awesome-typescript");3.1.2延迟计算/运行constadd=function(a,b){returna+b;};constcurred=_.curry(add);constplusOne=curried(1);上述代码中,通过对add函数进行“柯里化”处理,我们可以实现延迟计算。好了,简单介绍完柯里化的功能,我们来实现一个柯里化的功能。3.2柯里化的实现现在我们知道,当柯里化函数接收到足够的参数时,它就会开始执行原函数。并且如果接收到的参数不足,则返回一个新的函数来接收剩余的参数。基于以上特点,我们可以自己实现一个curry函数:函数的形参个数returnfunc.apply(this,args);}else{returnfunction(...args2){returncurred.apply(this,args.concat(args2));};}}}四、partial函数应用在计算机科学中,部分应用是指固定一个函数的一些参数,然后生成另一个具有更小元素的函数。所谓元素是指函数参数的个数。例如,具有一个参数的函数称为一元函数。部分应用(PartialApplication)很容易与函数柯里化混淆。它们的区别在于:partialfunctionapplication固定一个函数的一个或多个参数,返回一个可以接收剩余参数的函数;currying就是将函数转换成多个嵌套的一元函数,即每个函数只接受一个参数。了解了partialfunction和currying的区别之后,我们就用Lodash提供的partialfunction来了解一下它的使用方法。4.1使用部分函数functionbuildUri(scheme,domain,path){return`${scheme}://${domain}/${path}`;}constmyGithubPath=_.partial(buildUri,"https","github.com");constprofilePath=myGithubPath("semlinker/semlinker");constawesomeTsPath=myGithubPath("semlinker/awesome-typescript");_.partial(func,[partials])创建一个函数。该函数调用func,传入预设的partials参数。来源:https://www.lodashjs.com/docs/lodash.partial4.2偏函数的实现偏函数用于固定一个函数的一个或多个参数,返回一个可以接收剩余参数的函数。基于以上特点,我们可以自己实现一个偏函数:arguments));returnfn.apply(this,newArgs);};}4.3Partialfunctionimplementationvscurryingimplementation5.Lazyfunctions不同浏览器之间存在一些兼容性问题,导致我们使用一些WebAPI,你需要判断,例如:functionaddHandler(element,type,handler){if(element.addEventListener){element.addEventListener(type,handler,false);}elseif(element.attachEvent){element.attachEvent("on"+type,handler);}else{element["on"+type]=handler;}}在上面的代码中,我们实现了在不同浏览器中添加事件监听器的处理。代码实现起来也很简单,但是有个问题就是每次调用都需要判断,这显然不合理。对于上面的问题,我们可以通过懒加载功能来解决。5.1函数的延迟加载所谓延迟加载,就是函数第一次根据条件执行时,第二次调用函数时,不再检查条件,直接执行函数。要实现这个功能,我们可以在判断第一个条件的时候,覆盖掉满足判断条件的分支中的调用函数。具体实现方法如下:(element.attachEvent){addHandler=function(element,type,handler){element.attachEvent("on"+type,handler);};}else{addHandler=function(element,type,handler){element["on"+type]=handler;};}//保证第一次调用能正常执行加载:constaddHandler=(function(){if(document.addEventListener){returnfunction(element,type,handler){element.addEventListener(type,handler,false);};}elseif(document.attachEvent){returnfunction(element,类型,处理程序){element.attachEvent(“on”+type,handler);};}else{returnfunction(元素,type,handler){element[“on”+type]=handler;};}})();通过自执行函数,进行条件判断l在代码加载阶段被执行,然后在相应的条件分支中返回一个新的函数来实现相应的处理逻辑。6.缓存函数缓存函数就是缓存函数的计算结果。下次以相同参数调用该函数时,将直接返回缓存的结果,不再执行该函数。这是一种常见的以空间换时间的性能优化方法。实现缓存函数的功能,我们可以将序列化后的参数作为键,将第一次调用后的结果作为值存储在对象中。每次函数调用执行前,先判断缓存中是否有对应的key,如果有则直接返回key对应的值。分析完缓存函数的实现思路,我们来看看如何实现:functionmemorize(fn){constcache=Object.create(null);//用于存储缓存数据的对象returnfunction(...args){const_args=JSON.stringify(args);returncache[_args]||(cache[_args]=fn.apply(fn,args));};};定义了memorize缓存函数后,我们可以这样使用:letcomplexCalc=(a,b)=>{//执行复数计算};letmemoCalc=memorize(complexCalc);memoCalc(666,888);memoCalc(666,888);//从缓存中获取七、参考资源Wikipedia-Higher-orderfunctionwikiWikipedia-Curryingjavascript-functional-programming-explained-partial-application-and-currying