上一篇成为一名函数式码农(1)
友情提示
请仔细阅读文中的代码。确保你已经理解了代码之后再进行下一步。每一节都是建立在前一节的基础上。
如果你匆忙行事,你可能会错过一些对后面章节很重要的细微差别。
重构
我们先来看一下重构问题。这里有一些JavaScript代码片段:
<pre>
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');
}
</pre>
我们先前都这样写代码,一段时间后我们开始意识到这两个函数实际上功能是一样的只有一小部分不一样(已经以粗体显示)。
我们不应该复制粘贴validateSsn
然后修改一些代码来创建validatePhone
,而应该创建一个单独的函数通过参数来处理粘贴以后修改的部分。
在本例中我们应该通过参数处理value
,regular expression
和打印的message
(至少打印消息的最后一部分)。
重构后的代码:
<pre>
function validateValue(value, regex, type) {
if (regex.exec(value))
console.log('Invalid ' + type);
else
console.log('Valid ' + type);
}
</pre>
value
代表之前代码中的ssn
和phone
。
regex
代表之前的正则表达式/^\d{3}-\d{2}-\d{4}$/
和 /^\(\d{3}\)\d{3}-\d{4}$/
。
最后,消息的最后一部分即‘SSN’和‘Phone Number’由type
表示。
使用一个函数比使用两个函数或者更糟的3个、4个或者10个函数要好一些。这使得代码整洁并且可维护。
例如,如果发现一个bug,你只需要修改一个地方即可,而不是搜索整个代码库来查找可能粘贴并修改这个函数的地方。
但是如果有以下这样的情况会怎样:
<pre>
function validateAddress(address) {
if (parseAddress(address))
console.log('Valid Address');
else
console.log('Invalid Address');
}
function validateName(name) {
if (parseFullName(name))
console.log('Valid Name');
else
console.log('Invalid Name');
}
</pre>
在这parseAddress
和parseFullName
函数都是接受一个string
,如果传入的字符串符合预订的句法则返回true
。
我们怎么来重构这段代码?
好的,我们可以像前面一样使用value
来代表address
和name
,使用type
来代表‘Address’和‘Name’,但是我们的正则表达式之前是一个函数。
如果我们可以将一个函数作为参数传递。。。
高阶函数
许多语言不支持将函数作为参数传递。有的支持但是变得非常复杂。
在函数式语言中,函数是这个语言中的一等公民。换句话说,函数仅仅是另一种值而已。
由于函数仅仅是值,我们可以将它们作为参数传递。
尽管JavaScript不是纯粹的函数式语言,你可以用它做一些函数式操作。所以这里我们将前面两个函数重构成一个函数,将parsing function
(正则表达式匹配函数)通过参数parseFunc
传递进去:
<pre>
function validateValueWithFunc(value, parseFunc, type) {
if (parseFunc(value))
console.log('Invalid ' + type);
else
console.log('Valid ' + type);
}
</pre>
我们的新函数就称为高阶函数(Higher-order Function)。
高阶函数可以接收函数作为参数,或者返回一个函数结果,或者两者同时具备。
现在可以使用我们的高阶函数来替代前的4个函数(这个在JavaScript中可以工作,因为Regex.exec
匹配成功会返回true):
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');
这比前面使用4个几乎一模一样的函数要好多了。
但是请注意正则表达式。它们有一点啰嗦。我们再重构一下:
<pre>
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');
</pre>
好一点了。现在当我们想要分析一个电话号码时,我们不需要再复制粘贴正则表达式。
但是想象一下我们有更多的正则表达式要分析,不仅仅是parseSsn
和parsePhone
。每次我们创建一个正则表达式解析器,我们需要记住添加.exec
到它的末尾。说真的,对我来说很容易忘记。
我们可以通过创建一个返回exec
函数的高阶函数来防止这种情况:
<pre>
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');
</pre>
这里makeRegexParser
接收一个正则表达式并返回exec
函数,exec
函数接收一个字符串参数。validateValueWithFunc
函数会传递字符串(value
)给解析函数,即exec
。
的确,这是一个微不足道的改进,但是这里展示了一个高阶函数返回一个函数的例子。
但是,你可以想象一下如果makeRegexParser
比现在要复杂得多的时候,这个改进所带来的好处。
这里还有另外一个高阶函数返回函数的例子:
function makeAdder(constantValue) {
return function adder(value) {
return constantValue + value;
};
}
makeAdder
接收一个参数constantValue
返回一个adder
,adder是一个函数,它将给它接收的参数加上一个常量值。
这是它怎么被使用:
var add10 = makeAdder(10);
console.log(add10(20)); // prints 30
console.log(add10(30)); // prints 40
console.log(add10(40)); // prints 50
我们通过向makeAdder
函数(它会返回一个函数)传递一个常量10
来创建一个函数add10
,add10
将给任意值加上10
。
注意adder
函数可以访问constantValue
,即使makeAddr
函数已经返回。因为adder
在创建时constantValue
在它的作用域内。
这个行为非常的重要,因为如果没有它,能够返回函数的函数不是很有用。所以我们理解它是怎么工作以及怎么称呼它非常要。
这种行为成为闭包。
闭包(Closures)
这里有一个人为的使用闭包的函数例子:
<pre>
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;
};
};
}
</pre>
在这个例子中,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
这里,parentFunc
保持parent
的作用域存活,因为grandParent
返回的是parent
函数。
类似的,childFunc
保持child
的作用域存活,因为parentFunc
(这里就是parent
)返回child
函数。
当一个函数被创建,其整个生命周期中都是可以访问在在其创建时作用域内的所有变量。只要有引用指向它该函数就会一直存在。例如:只要childFunc
还引用它,child
的作用域就一直存在。
闭包就是一个函数的作用域,这个作用域通过指向该函数的引用保持存活。
注意在JavaScript中闭包是会造成问题的,因为这些变量是可修改的,即,从它们结束一直到他们返回的函数被调用期间,它们都可以修改变量。
幸亏,函数式语言中的变量都是不可修改的,这样可以消除这个常见的bug以及困惑根源。
下一篇:成为一名函数式码农(3)