理解函数式编程的概念是重要的第一步, 也可能是最困难的一步. 但不是说就一定得从概念起步. 不妨换个适合的视角.
上一篇: 第 1 部分
友情提示
请慢慢地阅读代码, 确保你能理解他们. 本文的每一节都依赖于上一节的内容.
如果你过于着急, 就可能错过一些重要的细节.
重构
让我们花点时间思考一下重构. 这里有一段 JavaScript 代码:
- function validateSsn(ssn) {
- if (/^\d{3}-\d{2}-\d{4}$/.exec(ssn))
- console.log('Valid SSN');
- else
- console.log('Invalid SSN');
- }
- function validatePhone(phone) {
- if (/^\(\d{3}\)\d{3}-\d{4}$/.exec(phone))
- console.log('Valid Phone Number');
- else
- console.log('Invalid Phone Number');
- }
我们都写过类似的代码, 随着时间的推移, 我们会认识到这两个函数实际基本上是相同的, 只有一点点不同(用 粗体 显示).
为了不使用拷贝粘贴的方式从 validateSsn 创建 validatePhone , 我们需要创建一个函数, 粘贴内容并进修改, 使之参数化.
在这个例子中, 可以抽象出 值(value) , 正则表达式(regex) 和打印的 消息(message) (至少是输出消息的最后一部分).
重构后的代码:
- function validateValue(value, regex, type) {
- if (regex.exec(value))
- console.log('Invalid' + type);
- else
- console.log('Valid' + type);
- }
旧代码中的参数 ssn 和 phone 现在由参数 value 传入.
正则表达式 /^\d{3}-\d{2}-\d{4}$/ 和 /^(\d{3})\d{3}-\d{4}$/ 由参数 regex 传入.
最后一, 消息的后面部分'SSN' 和'Phone Number' 由参数 type 传入.
用一个函数比用两个函数好得多, 就更不用说代替三, 四个, 甚至十个函数了. 这会让你的代码整洁且易于维护.
比如说, 如果存在 BUG, 你只需要修改一个地方, 而不是在整个代码库中搜索这个函数可能被在哪些方被粘贴修改过.
但是如果遇到下面这样的情况该怎么办:
- function validateAddress(address) {
- if (parseAddress(address))
- console.log('Valid Address');
- else
- console.log('Invalid Address');
- }
- function validateName(name) {
- if (arseFullName(name))
- console.log('Valid Name');
- else
- console.log('Invalid Name');
- }
这里 parseAddress 和 parseFullName 都是需要一个 string 参数的函数, 而且如果解析成功都返回 true .
该如何重构呢?
我们可以像之前那样, 把 address 和 name 作为 value 传入, 而'Address' 和'Name' 作为 type , 然后在传入正则表达式的地方传入函数.
既然我们可以把函数作为参数传入, 那还有啥好说的......
高阶函数
许多语言并不支持将函数作为参数传递. 一些 (语言) 虽然支持, 但过程繁琐.
在函数式编程中, 函数便是该语言一等公民. 换言之, 一个函数只是另一种值的表现方式.
因为函数只是一些值而已, 那么我们便可把它们当做参数进行传递.
尽管 JavaScript 不是纯函数式语言, 你依然可以用它做一些函数式操作. 那么如下便是最后两个函数的重构结果, 通过将那个名为 parseFunc 的 转换函数 作为参数进行传递:
- function validateValueWithFunc(value, parseFunc, type) {
- if (parseFunc(value))
- console.log('Invalid' + type);
- else
- console.log('Valid' + type);
- }
我们的新函数就是一个 高阶函数.
高阶函数不仅可以将函数作为参数, 还可以将函数作为结果返回.
现在我们可以调用我们的高阶函数来实现之前四个函数的功能(这在 JavaScript 中有效, 因为当找到匹配时 Regex.exec 返回一个真值):
- 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 ** 函数, 该函数接受一个字符串. validateValueWithFuncwill 将字符串 value 传递给 parse 函数, 即 exec .
parseSsn 和 parsePhone 实际上和以前一样, 是正则表达式的 exec 函数.
当然, 这是一个微小的改进, 但放到这里是为了给出一个返回函数的高阶函数的示例.
但是, 如果 makeRegexParser 更复杂的话, 你可以想象下做如此更改的好处.
这是返回函数的高阶函数的另一个示例:
- function makeAdder(constantValue) {
- return function adder(value) {
- return constantValue + value;
- };
- }
这里我们定义了 makeAdder , 它接收 constantValue 作为参数并返回 adder -- 一个可以将传递给它的任意值加上给定常量的函数.
下面是它是如何被使用的示例:
- var add10 = makeAdder(10);
- console.log(add10(20)); _// prints 30
- _console.log(add10(30)); _// prints 40
- _console.log(add10(40)); _// prints 50_
我们通过将常量 10 传递给 makeAdder 来创建一个 add10 函数, 该函数会返回一个将所有值都 + 10 的函数.
请注意, 即使在 makeAddr 返回后, 函数 adder 也可以访问 constantValue . 那是因为当创建 adder 时, constantValue 在其作用域之内.
这种行为非常重要, 因为如果没有它, 返回函数的函数将不会非常有用. 因此, 重要的是我们要了解它们的工作方式以及此类行为的术语.
这种行为被称为 Closure .
Closures 闭包
这是一个使用闭包的函数的人为设计的例子:
- 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 的变量.
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
这里, 在 grandParent 返回 parent 之前, parentFunc 将在 parent 作用域内有效.
同样地, 在 parentFunc, 亦即 parent 返回 child 之前, childFunc 将在 child 作用域内有效.
创建函数时, 在函数生命周期内, 它可以访问在其创建时其作用域内的所有变量. 只要仍然存在对某函数的引用, 该函数就是存在的. 例如, 只要 childFunc 仍引用 child , 那么它的作用域就是存在的.
闭包是一个函数的作用域, 它通过对该函数的引用保证其可见性.
请注意, 在 JavaScript 中, 闭包是存在问题的, 因为变量是可变的, 即它们可以在封闭它们到调用返回函数的时间内改变值.
值得庆幸的是, 函数式语言中的变量是不可变的, 这规避了这种常见的错误和混淆源.
我的脑袋!!!!
到现在为止足够了.
在本文的后续文章中, 我将探讨函数式组合, Currying, 通用函数式函数 (例如地图, 过滤器, 折叠等) 等内容.
来源: http://www.tuicool.com/articles/UB7RR3I