让我们花点时间思考一下重构。这里有一段 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函数。
下面是它是如何被使用的示例:
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。
这是一个使用闭包的函数的人为设计的例子:
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中,闭包是存在问题的,因为变量是可变的,即它们可以在封闭它们到调用返回函数的时间内改变值。
值得庆幸的是,函数式语言中的变量是不可变的,这规避了这种常见的错误和混淆源。
评论删除后,数据将无法恢复
评论(5)