摘要: JS函数式编程入门。javascript
Fundebug经受权转载,版权归原做者全部。前端
在这篇由多部分组成的文章中,接下来将介绍函数式编程的一些概念,这些概念对你学习函数式编程有所帮助。若是你已经懂了什么是函数式编程,这能够加深你的理解。java
请不要着急。从这一点开始,花点时间阅读并理解代码示例。你甚至可能想在每节课结束后中止阅读,以便让你的观点深刻理解,而后再回来完成。程序员
最重要的是你要理解。ajax
所谓纯函数,就是指这样一个函数,对于相同的输入,永远获得相同的输出,它不依赖外部环境,也不会改变外部环境。若是不知足以上几个条件那就是非纯函数。正则表达式
下面是Javascript中的一个纯函数示例:sql
var z = 10; function add(x, y) { return x + y; }
注意,add
函数不涉及z
变量。它不从z
读取,也不从z
写入,它只读取x
和y
,而后返回它们相加的结果。这是一个纯函数。若是 add 函数确实访问了变量z,那么它就再也不是纯函数了。数据库
请思考一下下面这个函数:编程
function justTen() { return 10; }
若是函数justTen
是纯的,那么它只能返回一个常量, 为何?小程序
由于咱们没有给它任何参数。 并且,既然是纯函数的,除了本身的输入以外它不能访问任何东西,它惟一能够返回的就是常量。
因为不带参数的纯函数不起做用,因此它们不是颇有用。因此justTen
被定义为一个常数会更好。
大多数有用的纯函数必须至少带一个参数。
考虑一下这个函数:
function addNoReturn(x, y) { var z = x + y }
注意这个函数是不返回任何值。它只是把变量x
和y
相加赋给变量z
,但并无返回。
这个也是一个纯函数,由于它只处理输入。它确实对输入的变量进行操做,可是因为它不返回结果,因此它是无用的。
全部有用的纯函数都必须返回一些咱们指望的结果。
让咱们再次考虑第一个add函数:
注意 add(1, 2) 的返回结果老是 3。这不是奇怪的事情,只是由于 add 函数是纯的。若是 add 函数使用了一些外部值,那么你永远没法预测它的行为。
在给定相同输入的状况下,纯函数老是返回相同的结果。
因为纯函数不能改变任何外部变量,因此下面的函数都不是纯函数:
writeFile(fileName); updateDatabaseTable(sqlCmd); sendAjaxRequest(ajaxRequest); openSocket(ipAddress);
全部这些功能都有反作用。当你调用它们时,它们会更改文件和数据库表、将数据发送到服务器或调用操做系统以获取套接字。它们不只对输入操做同时也对输出进行操做,所以,你永远没法预测这些函数将返回什么。
纯函数没有反作用。
在Javascript、Java 和 c# 等命令式编程语言中,反作用无处不在。这使得调试很是困难,由于变量能够在程序的任何地方更改。因此,当你有一个错误,由于一个变量在错误的时间被更改成错误的值,这不是很好。
此时,你可能会想,“我怎么可能只使用纯函数呢?”
函数式编程不能消除反作用,只能限制反作用。因为程序必须与真实环境相链接,因此每一个程序的某些部分确定是不纯的。函数式编程的目标是尽可能写更多的纯函数,并将其与程序的其余部分隔离开来。
你还记得你第一次看到下面的代码是何时吗?
var x = 1; x = x + 1;
教你初中数学的老师看到以上代码,可能会问你,你忘记我给你教的数学了吗? 由于在数学中,x 永远不能等于x + 1。
但在命令式编程中,它的意思是,取x
的当前值加1,而后把结果放回x
中。
在函数式编程中,x = x + 1是非法的。因此这里你能够用数学的逻辑还记得在数式编程中这样写是不对的!
函数式编程中没有变量。
因为历史缘由,存储值的变量仍然被称为变量,但它们是常量,也就是说,一旦x
取值,这个常量就是x
返回的值。别担忧,x
一般是一个局部变量,因此它的生命周期一般很短。但只要它还没被销毁,它的值就永远不会改变。
下面是Elm
中的常量变量示例,Elm
是一种用于Web开发的纯函数式编程语言:
addOneToSum y z = let x = 1 in x + 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,它更容易阅读,以下:
sumRange start end acc = if start > end then acc else sumRange (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 的参数:
function validateValueWithFunc(value, parseFunc, type) { if (parseFunc(value)) console.log('Invalid ' + type); else console.log('Valid ' + type); }
像函数 parseFunc 接收一个或多个函数做为输入的函数,称为 高阶函数。
高阶函数要么接受函数做为参数,要么返回函数,要么二者兼而有之。
如今能够调用高阶函数(这在Javascript中有效,由于Regex.exec在找到匹配时返回一个truthy值):
validateValueWithFunc('123-45-6789', /^\d{3}-\d{2}-\d{4}$/.exec, 'SSN'); validateValueWithFunc('(123)456-7890', /^\(\d{3}\)\d{3}-\d{4}$/.exec, 'Phone'); validateValueWithFunc('123 Main St.', parseAddress, 'Address'); validateValueWithFunc('Joe Mama', parseName, 'Name');
这比有四个几乎相同的函数要好得多。
可是请注意正则表达式,这里有点冗长了。简化一下:
var parseSsn = /^\d{3}-\d{2}-\d{4}$/.exec; var parsePhone = /^\(\d{3}\)\d{3}-\d{4}$/.exec; validateValueWithFunc('123-45-6789', parseSsn, 'SSN'); validateValueWithFunc('(123)456-7890', parsePhone, 'Phone'); validateValueWithFunc('123 Main St.', parseAddress, 'Address'); validateValueWithFunc('Joe Mama', parseName, 'Name');
如今看起来好多了。如今,当要验证一个电话号码时,不须要复制和粘贴正则表达式了。
可是假设咱们有更多的正则表达式须要解析,而不只仅是 parseSsn 和 parsePhone。每次建立正则表达式解析器时,咱们都必须记住在末尾添加 .exec,这很容易被忘记。
能够经过建立一个返回exec 的高阶函数来防止这种状况:
function makeRegexParser(regex) { return regex.exec; } var parseSsn = makeRegexParser(/^\d{3}-\d{2}-\d{4}$/); var parsePhone = makeRegexParser(/^\(\d{3}\)\d{3}-\d{4}$/); validateValueWithFunc('123-45-6789', parseSsn, 'SSN'); validateValueWithFunc('(123)456-7890', parsePhone, 'Phone'); validateValueWithFunc('123 Main St.', parseAddress, 'Address'); validateValueWithFunc('Joe Mama', parseName, 'Name');
这里,makeRegexParser采用正则表达式并返回exec函数,该函数接受一个字符串。validateValueWithFunc 将字符串 value 传递给 parse 函数,即exec。
parseSsn 和 parsePhone 实际上与之前同样,是正则表达式的 exec 函数。
固然,这是一个微小的改进,可是这里给出了一个返回函数的高阶函数示例。可是,若是makeRegexParser 要复杂得多,这种更改的好处是很大的。
下面是另外一个返回函数的高阶函数示例:
function makeAdder(constantValue) { return function adder(value) { return constantValue + value; }; }
函数 makeAdder,接受参数 constantValue 并返回函数 adder,这个函数返回 constantValue 与它传入参数相加结果。
下面是它的用法:
var add10 = makeAdder(10); console.log(add10(20)); // 打印 30 console.log(add10(30)); // 打印 40 console.log(add10(40)); // 打印 50
咱们经过将常量10传递给 makeAdder 来建立一个函数 add10, makeAdder 返回一个函数,该函数将向返回的结果都加 10。
注意,即便在 makeAddr 返回以后,函数 adder 也能够访问变量 constantValue。 这里能访问到 constantValue 是由于存在闭包。
闭包机制很是重要,由于若是没有它 ,返回函数的函数就不会有很大做用。因此必须了解它们是如何工做。
下面是一个使用闭包的函数的示例:
function grandParent(g1, g2) { var g3 = 3; return function parent(p1, p2) { var p3 = 33; return function child(c1, c2) { var c3 = 333; return g1 + g2 + g3 + p1 + p2 + p3 + c1 + c2 + c3; }; }; }
在这个例子中,child 函数能够访问它自身的变量,函数 parent 函数能够访问它的自身变量和函数 grandParent 的变量。而函数 grandParent 只能访问自身的变量。
下面是它的一个使用例子:
var parentFunc = grandParent(1, 2); // returns parent() var childFunc = parentFunc(11, 22); // returns child() console.log(childFunc(111, 222)); // prints 738 // 1 + 2 + 3 + 11 + 22 + 33 + 111 + 222 + 333 == 738
在这里,parentFunc 保留了 parent 的做用域,由于 grandParent 返回 parent。
相似地,childFunc 保留了 child 的做用域,由于 parentFunc 保留了 parent 的做用域,而 parent 的做用域 保留了child 的做用域。
当一个函数被建立时,它在建立时做用域中的全部变量在函数的生命周期内都是可访问的。一个函数只要还有对它的引用就存在。例如,只要childFunc 还引用 child 的做用域,child 的做用域就存在。
闭包具体还看看以前整理的一篇文章:我历来不理解JavaScript闭包,直到有人这样向我解释它...
原文:
一、https://medium.com/@cscalfani...
二、https://medium.com/@cscalfani...
编辑中可能存在的bug无法实时知道,过后为了解决这些bug,花了大量的时间进行log 调试,这边顺便给你们推荐一个好用的BUG监控工具Fundebug。
你的点赞是我持续分享好东西的动力,欢迎点赞!
一个笨笨的码农,个人世界只能终身学习!
更多内容请关注公众号《大迁世界》!
Fundebug专一于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了9亿+错误事件,获得了Google、360、金山软件、百姓网等众多知名用户的承认。欢迎免费试用!
转载时请注明做者Fundebug以及本文地址: https://blog.fundebug.com/2018/12/27/to-be-a-functional-programmer-part-1/