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

JavaScript中的函数式编程:函数、组合和柯里化

时间:2023-03-12 20:14:10 科技观察

面向对象编程和函数式编程是两种截然不同的编程范式,各有各的规则和优缺点。然而,JavaScript并不总是遵循一个规则,它恰好位于两个规则的中间,提供了普通OOP语言的某些方面,如类、对象、继承等。但同时,它也提供了你有一些函数式编程的概念,比如高阶函数和组合它们的能力。高阶函数我们从三个概念中最重要的一个开始:高阶函数。高阶函数意味着函数不仅仅是可以从代码中定义和调用的函数,事实上,您可以将它们用作可分配的实体。如果您使用过一些JavaScript,那么这应该不足为奇。将匿名函数分配给常量是很常见的。constadder=(a,b)=>{returna+b}上面的逻辑在很多其他语言中是无效的,能够把函数当成整数来赋值是一个非常有用的工具,实际上本文涵盖的大部分主题是这个功能的副产品。高阶函数的好处:封装行为有了高阶函数,我们不仅可以像上面那样给函数赋值,还可以在调用函数时将它们作为参数传递。这为创建一个非常动态的代码库打开了大门,通过直接将其作为参数传递,可以在该代码库上重用复杂的行为。想象一下在一个纯面向对象的环境中工作,并且您想要扩展一个类的功能以完成一项任务。在这种情况下,您可以通过将该实现逻辑封装在一个抽象类中来使用继承,然后将其扩展为一组实现类。这是完美的OOP行为,它有效,我们:创建一个抽象结构来封装我们的可重用逻辑创建一个二级构造我们重用原始类并扩展它现在,我们想要最重要的是重用逻辑,我们可以简单地提取将可重用逻辑放入一个函数中,然后将该函数作为参数传递给任何其他函数,这样,您可以节省一些创建“样板”的过程,因为,我们只是在创建函数。下面的代码展示了如何在OOP中重用程序逻辑。//封装行为封装行为stractclassLogFormatter{format(msg){returnDate.now()+"::"+msg}}//复用行为classConsoleLoggerextendsLogFormatter{log(msg){console.log(this.format(msg))}}classFileLoggerextendsLogFormatter{log(msg){writeToFileSync(this.logFile,this.format(msg))}}第二个提示是将逻辑提取到一个函数中,我们可以混合搭配以轻松创建我们需要的内容。您可以继续添加更多格式化和编写函数,然后只需将它们与一行代码混合在一起://GenericBehavioralAbstractionfunctionformat(msg){returnDate.now()+"::"+msg}functionconsoleWriter(msg){console.log(msg)}functionfileWriter(msg){letlogFile="logfile.log"writeToFileSync(logFile,msg)}functionlogger(output,format){returnmsg=>{output(format(msg))}}//使用它通过组合函数constconsoleLogger=logger(consoleWriter,format)constfileLogger=logger(fileWriter,format)两种方法各有优势,而且都非常有效,没有一个是最好的。只是为了展示这种方法的灵活性,我们能够将行为(即函数)作为参数传递,就好像它们是原始类型(如整数或字符串)一样。高阶函数的好处:代码简洁对于这个好处,一个很好的例子是Array方法,例如forEach、map、reduce等。在C等非函数式编程语言中,遍历数组元素并转换它们需要使用for循环或其他一些循环结构。这就需要我们按照规定的方式编写代码,也就是需求描述周期发生的过程。letmyArray=[1,2,3,4]lettransformedArray=[]for(leti=0;ix*2;letmyArray=[1,2,3,4];lettransformedArray=myArray.map(double);与第一种方式相比,这种方式更容易阅读,并且由于逻辑隐藏在两个函数(map和double)中,您不必担心理解它们是如何工作的。在第一个示例中,您还可以将乘法逻辑隐藏在函数内部,但遍历逻辑必须存在,这给阅读增加了一些不必要的障碍。Currying函数Currying是将接受多个参数的函数转换为接受单个参数(原始函数的第一个参数)的函数,并返回一个接受其余参数并返回结果的新函数的技术。让我们看一个例子:functionadder(a,b){returna+b}//变成constadd10=x=>adder(a,10)现在,如果你想做的只是将一系列值加10,那么它可以调用add10而不是每次都使用相同的第二个参数调用adder。这个例子可能看起来很愚蠢,但它体现了柯里化的理想。你可以把柯里化看成是函数式编程的继承,再回到logger的例子,这样可以得到:{console.log(msg)}functionfileOutput(msg){letfilename="mylogs.log"writeFileSync(msg,filename)}constlogger=msg=>log(msg,">>",consoleOutput);constfileLogger=msg=>日志(味精,“::”,文件输出);log的函数需要三个参数,我们将它引入一个只需要一个参数的专用版本,因为其他两个参数已经由我们选择了。注意,这里把日志函数当做抽象类,只是因为在我的例子中,不想直接使用它,但也没有限制,因为这只是一个普通的函数。如果我们使用一个类,我们将无法直接实例化它。函数组合函数组合是组合两个或多个函数以生成新函数的过程。将功能分组在一起就像将一连串管道拼接在一起让数据流过。在计算机科学中,函数组合是将简单函数组合成更复杂函数的行为或机制。就像数学中通常的函数组合一样,每个函数的结果作为参数传递给下一个函数,最后一个函数的结果是整个函数的结果。这是维基百科对函数组合的定义,粗体部分是比较关键的部分。使用柯里化时,就没有这样的限制,我们可以很方便地使用预设的函数参数。代码重用听上去很美好,但实现起来却很难。如果代码过于针对业务,将很难重用。比如代码太普通太简单,用的人少。所以我们需要两者之间的平衡,一种制作更小、可重复使用的部件的方法,我们可以将这些部件用作构建块来构建更复杂的功能。在函数式编程中,函数是我们的构建块。每个功能都有自己的功能,然后我们组合需要的功能(函数)来满足我们的需求。这种方法有点像乐高积木,我们在编程中称之为组合函数。看下面两个函数:varadd10=function(value){returnvalue+10;};varmult5=function(value){returnvalue*5;};上面的写法有点冗长,我们改写成箭头函数:varadd10=value=>value+10;varmult5=value=>value*5;现在需要一个函数把传入的参数先加10,再乘以5,如下:现在需要一个函数,把传入的参数先加10,再乘以5,如下:varmult5AfterAdd10=value=>5*(value+10)虽然这是一个非常简单的例子,但我还是不想从头开始写这个函数。首先,这里可能会犯错误,比如忘记了括号。其次,我们已经有了一个加10的函数add10和一个乘以5的函数mult5,所以这里我们要编写已经重复过的代码。重构mult5AfterAdd10以使用函数add10、mult5:varmult5AfterAdd10=value=>mult5(add10(value));我们只是使用现有函数来创建mult5AfterAdd10,但还有更好的方法。在数学中,f°g是函数组合,称为“f由g组成”,或者更常见的是“fafterg”。因此(f°g)(x)等价于f(g(x)),这意味着在调用f之后调用g。在我们的例子中,我们有mult5°add10或“add10aftermult5”,因此我们的函数名称称为mult5AfterAdd10。由于Javascript本身不做函数组合,看Elm是怎么写的:add10value=value+10mult5value=value*5mult5AfterAdd10value=(mult5<{varnewArray=[];for(vari=0;iv*10,things);这里没有for循环!而且代码更易读,更容易分析。现在让我们编写另一个常用函数来过滤数组中的元素:varfilter=(pred,array)=>{varnewArray=[];for(vari=0;ix%2!==0;varnumbers=[1,2,3,4,5];varoddNumbers=filter(isOdd,numbers);console.log(oddNumbers);//[1,3,5]filter函数比用for循环手工编程要简单的多。最后一个常用函数称为reduce。通常这个函数用于将一串数字化简为一个值,但实际上它可以做很多事情。在函数式语言中,这个函数叫做fold。varreduce=(f,start,array)=>{varacc=start;for(vari=0;i