北京白癜风治疗医院 http://wapyyk.39.net/bj/zhuanke/89ac7.html 摘要:JS函数式编程入门。 原文:学会使用函数式编程的程序员(第1部分) 作者:前端小智 在这篇由多部分组成的文章中,接下来将介绍函数式编程的一些概念,这些概念对你学习函数式编程有所帮助。如果你已经懂了什么是函数式编程,这可以加深你的理解。 请不要着急。从这一点开始,花点时间阅读并理解代码示例。你甚至可能想在每节课结束后停止阅读,以便让你的观点深入理解,然后再回来完成。 最重要的是你要理解。 纯函数(Purity)所谓纯函数,就是指这样一个函数,对于相同的输入,永远得到相同的输出,它不依赖外部环境,也不会改变外部环境。如果不满足以上几个条件那就是非纯函数。 下面是Javascript中的一个纯函数示例: varz=10;functionadd(x,y){returnx+y;} 注意,add函数不涉及z变量。它不从z读取,也不从z写入,它只读取x和y,然后返回它们相加的结果。这是一个纯函数。如果add函数确实访问了变量z,那么它就不再是纯函数了。 请思考一下下面这个函数: functionjustTen(){return10;} 如果函数justTen是纯的,那么它只能返回一个常量,为什么? 因为我们没有给它任何参数。而且,既然是纯函数的,除了自己的输入之外它不能访问任何东西,它唯一可以返回的就是常量。 由于不带参数的纯函数不起作用,所以它们不是很有用。所以justTen被定义为一个常数会更好。 大多数有用的纯函数必须至少带一个参数。 考虑一下这个函数: functionaddNoReturn(x,y){varz=x+y} 注意这个函数是不返回任何值。它只是把变量x和y相加赋给变量z,但并没有返回。 这个也是一个纯函数,因为它只处理输入。它确实对输入的变量进行操作,但是由于它不返回结果,所以它是无用的。 所有有用的纯函数都必须返回一些我们期望的结果。 让我们再次考虑第一个add函数: 注意add(1,2)的返回结果总是3。这不是奇怪的事情,只是因为add函数是纯的。如果add函数使用了一些外部值,那么你永远无法预测它的行为。 在给定相同输入的情况下,纯函数总是返回相同的结果。 由于纯函数不能改变任何外部变量,所以下面的函数都不是纯函数: writeFile(fileName);updateDatabaseTable(sqlCmd);sendAjaxRequest(ajaxRequest);openSocket(ipAddress); 所有这些功能都有副作用。当你调用它们时,它们会更改文件和数据库表、将数据发送到服务器或调用操作系统以获取套接字。它们不仅对输入操作同时也对输出进行操作,因此,你永远无法预测这些函数将返回什么。 纯函数没有副作用。 在Javascript、Java和c#等命令式编程语言中,副作用无处不在。这使得调试非常困难,因为变量可以在程序的任何地方更改。所以,当你有一个错误,因为一个变量在错误的时间被更改为错误的值,这不是很好。 此时,你可能会想,“我怎么可能只使用纯函数呢?” 函数式编程不能消除副作用,只能限制副作用。由于程序必须与真实环境相连接,所以每个程序的某些部分肯定是不纯的。函数式编程的目标是尽量写更多的纯函数,并将其与程序的其他部分隔离开来。 不可变性(Immutability)你还记得你第一次看到下面的代码是什么时候吗? varx=1;x=x+1; 教你初中数学的老师看到以上代码,可能会问你,你忘记我给你教的数学了吗?因为在数学中,x永远不能等于x+1。 但在命令式编程中,它的意思是,取x的当前值加1,然后把结果放回x中。 在函数式编程中,x=x+1是非法的。所以这里你可以用数学的逻辑还记得在数式编程中这样写是不对的! 函数式编程中没有变量。 由于历史原因,存储值的变量仍然被称为变量,但它们是常量,也就是说,一旦x取值,这个常量就是x返回的值。别担心,x通常是一个局部变量,所以它的生命周期通常很短。但只要它还没被销毁,它的值就永远不会改变。 下面是Elm中的常量变量示例,Elm是一种用于Web开发的纯函数式编程语言: addOneToSumyz=letx=1inx+y+z 如果你不熟悉ml风格的语法,让我解释一下。addOneToSum是一个函数,有两个参数分别为y和z。 在let块中,x被绑定到1的值上,也就是说,它在函数的生命周期内都等于1。当函数退出时,它的生命周期结束,或者更准确地说,当let块被求值时,它的生命周期就结束了。 在in块中,计算可以包含在let块中定义的值,即x,返回计算结果x+y+z,或者更准确地说,返回1+y+z,因为x=1。 你可能又会想:“我怎么能在没有变量的情况下做任何事情呢?” 我们想一下什么时候需要修改变量。通常会想到两种情况:多值更改(例如修改或记录对象中的单个值)和单值更改(例如循环计数器)。 函数式编程使用参数保存状态,最好的例子就是递归。是的,是没有循环。“什么没有变量,现在又没有循环?”我讨厌你!!!” 哈哈,这并不是说我们不能做循环,只是没有特定的循环结构,比如for,while,do,repeat等等。 函数式编程使用递归进行循环。 这里有两种方法可以在Javascript中执行循环: 注意,递归是一种函数式方法,它通过使用一个结束条件start(start+1)和调用自己accumulator(acc+start)来实现与for循环相同的功能。它不会修改旧的值。相反,它使用从旧值计算的新值。 不幸的是,这在Javascript中很难想懂,需要你花点时间研究它,原因有二。第一,Javascript的语法相对其它高级语言比较乱,其次,你可能还不习惯递归思维。 在Elm,它更容易阅读,如下: sumRangestartendacc=ifstartendthenaccelsesumRange(start+1)end(acc+start) 它是这样运行的: 你可能认为for循环更容易理解。虽然这是有争议的,而且更可能是一个熟悉的问题,但非递归循环需要可变性,这是不好的。 在这里,我还没有完全解释不变性的好处,但是请查看全局可变状态部分,即为什么程序员需要限制来了解更多。 我还没有完全解释不可变性(Immutability)在这里的好处,但请查看为什么程序员需要限制的全局可变状态部分以了解更多信息。 不可变性的好处是,你读取访问程序中的某个值,但只有读权限的,这意味着不用害怕其他人更改该值使自己读取到的值是错误。 不可变性的还有一个好处是,如果你的程序是多线程的,那么就没有其他线程可以更改你线程中的值,因为该值是不可变,所以另一个线程想要更改它,它只能从旧线程创建一个新值。 不变性可以创建更简单、更安全的代码。 重构让我们考虑一下重构,下面是一些Javascript代码: 我们以前可能都写过这样的代码,随着时间的推移,开始意识到这两个函数实际上是相同的,函数名称,打印结果不太一样而已。 我们不应该复制validateSsn来创建validatePhone,而是应该创建一个函数(共同的部分),通过参数形式实现我们想要的结果。 重构后的代码如下: 旧代码参数中ssn和phone现在用value表示,正则表达式/^\d{3}-\d{2}-\d{4}/and/^(\d{3})\d{3}-\d{4}/由变量regex.表示。最后,消息“SSN”和“电话号码”由变量type表示。 这个有类似的函数都可以使用这个函数来实现,这样可以保持代码的整洁和可维护性。 高阶函数许多语言不支持将函数作为参数传递,有些会支持但并不容易。 在函数式编程中,函数是一级公民。换句话说,函数通常是另一个函数的值。 由于函数只是值,我们可以将它们作为参数传递。即使Javascript不是纯函数语言,也可以使用它进行一些功能性的操作。所以这里将上面的两个函数重构为单个函数,方法是将验证合法性的函数作为函数parseFunc的参数: functionvalidateValueWithFunc(value,parseFunc,type){if(parseFunc(value))console.log(Invalid+type);elseconsole.log(Valid+type);} 像函数parseFunc接收一个或多个函数作为输入的函数,称为高阶函数。 高阶函数要么接受函数作为参数,要么返回函数,要么两者兼而有之。 现在可以调用高阶函数(这在Javascript中有效,因为Regex.exec在找到匹配时返回一个truthy值): validateValueWithFunc(-45-,/^\d{3}-\d{2}-\d{4}/.exec,SSN);validateValueWithFunc(()-,/^\(\d{3}\)\d{3}-\d{4}/.exec,Phone);validateValueWithFunc(MainSt.,parseAddress,Address);validateValueWithFunc(JoeMama,parseName,Name); 这比有四个几乎相同的函数要好得多。 但是请注意正则表达式,这里有点冗长了。简化一下: varparseSsn=/^\d{3}-\d{2}-\d{4}/.exec;varparsePhone=/^\(\d{3}\)\d{3}-\d{4}/.exec;validateValueWithFunc(-45-,parseSsn,SSN);validateValueWithFunc(()-,parsePhone,Phone);validateValueWithFunc(MainSt.,parseAddress,Address);validateValueWithFunc(JoeMama,parseName,Name); 现在看起来好多了。现在,当要验证一个电话号码时,不需要复制和粘贴正则表达式了。 但是假设我们有更多的正则表达式需要解析,而不仅仅是parseSsn和parsePhone。每次创建正则表达式解析器时,我们都必须记住在末尾添加.exec,这很容易被忘记。 可以通过创建一个返回exec的高阶函数来防止这种情况: functionmakeRegexParser(regex){returnregex.exec;}varparseSsn=makeRegexParser(/^\d{3}-\d{2}-\d{4}/);varparsePhone=makeRegexParser(/^\(\d{3}\)\d{3}-\d{4}/);validateValueWithFunc(-45-,parseSsn,SSN);validateValueWithFunc(()-,parsePhone,Phone);validateValueWithFunc(MainSt.,parseAddress,Address);validateValueWithFunc(JoeMama,parseName,Name); 这里,makeRegexParser采用正则表达式并返回exec函数,该函数接受一个字符串。validateValueWithFunc将字符串value传递给parse函数,即exec。 parseSsn和parsePhone实际上与以前一样,是正则表达式的exec函数。 当然,这是一个微小的改进,但是这里给出了一个返回函数的高阶函数示例。但是,如果makeRegexParser要复杂得多,这种更改的好处是很大的。 下面是另一个返回函数的高阶函数示例: functionmakeAdder(constantValue){returnfunctionadder(value){returnconstantValue+value;};} 函数makeAdder,接受参数constantValue并返回函数adder,这个函数返回constantValue与它传入参数相加结果。 下面是它的用法: varadd10=makeAdder(10);console.log(add10(20));//打印30console.log(add10(30));//打印40console.log(add10(40));//打印50 我们通过将常量10传递给makeAdder来创建一个函数add10,makeAdder返回一个函数,该函数将向返回的结果都加10。 注意,即使在makeAddr返回之后,函数adder也可以访问变量constantValue。这里能访问到constantValue是因为存在闭包。 闭包机制非常重要,因为如果没有它,返回函数的函数就不会有很大作用。所以必须了解它们是如何工作。 闭包下面是一个使用闭包的函数的示例: functiongrandParent(g1,g2){varg3=3;returnfunctionparent(p1,p2){varp3=33;returnfunctionchild(c1,c2){varc3=;returng1+g2+g3+p1+p2+p3+c1+c2+c3;};};} 在这个例子中,child函数可以访问它自身的变量,函数parent函数可以访问它的自身变量和函数grandParent的变量。而函数grandParent只能访问自身的变量。 下面是它的一个使用例子: varparentFunc=grandParent(1,2);//returnsparent()varchildFunc=parentFunc(11,22);//returnschild()console.log(childFunc(,));//prints//1+2+3+11+22+33+++== 在这里,parentFunc保留了parent的作用域,因为grandParent返回parent。 类似地,childFunc保留了child的作用域,因为parentFunc保留了parent的作用域,而parent的作用域保留了child的作用域。 当一个函数被创建时,它在创建时作用域中的所有变量在函数的生命周期内都是可访问的。一个函数只要还有对它的引用就存在。例如,只要childFunc还引用child的作用域,child的作用域就存在。 闭包具体还看看之前整理的一篇文章:我从来不理解JavaScript闭包,直到有人这样向我解释它… 原文:1、
|