
友情提示

請仔細閱讀文中的代碼。確保你已經(jīng)理解了代碼之后再進行下一步。每一節(jié)都是建立在前一節(jié)的基礎上。
如果你匆忙行事,你可能會錯過一些對后面章節(jié)很重要的細微差別。
重構

我們先來看一下重構問題。這里有一些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>
我們先前都這樣寫代碼,一段時間后我們開始意識到這兩個函數(shù)實際上功能是一樣的只有一小部分不一樣(已經(jīng)以粗體顯示)。
我們不應該復制粘貼validateSsn然后修改一些代碼來創(chuàng)建validatePhone,而應該創(chuàng)建一個單獨的函數(shù)通過參數(shù)來處理粘貼以后修改的部分。
在本例中我們應該通過參數(shù)處理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表示。
使用一個函數(shù)比使用兩個函數(shù)或者更糟的3個、4個或者10個函數(shù)要好一些。這使得代碼整潔并且可維護。
例如,如果發(fā)現(xiàn)一個bug,你只需要修改一個地方即可,而不是搜索整個代碼庫來查找可能粘貼并修改這個函數(shù)的地方。
但是如果有以下這樣的情況會怎樣:
<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函數(shù)都是接受一個string,如果傳入的字符串符合預訂的句法則返回true。
我們怎么來重構這段代碼?
好的,我們可以像前面一樣使用value來代表address和name,使用type來代表‘Address’和‘Name’,但是我們的正則表達式之前是一個函數(shù)。
如果我們可以將一個函數(shù)作為參數(shù)傳遞。。。
高階函數(shù)

許多語言不支持將函數(shù)作為參數(shù)傳遞。有的支持但是變得非常復雜。
在函數(shù)式語言中,函數(shù)是這個語言中的一等公民。換句話說,函數(shù)僅僅是另一種值而已。
由于函數(shù)僅僅是值,我們可以將它們作為參數(shù)傳遞。
盡管JavaScript不是純粹的函數(shù)式語言,你可以用它做一些函數(shù)式操作。所以這里我們將前面兩個函數(shù)重構成一個函數(shù),將parsing function(正則表達式匹配函數(shù))通過參數(shù)parseFunc傳遞進去:
<pre>
function validateValueWithFunc(value, parseFunc, type) {
if (parseFunc(value))
console.log('Invalid ' + type);
else
console.log('Valid ' + type);
}
</pre>
我們的新函數(shù)就稱為高階函數(shù)(Higher-order Function)。
高階函數(shù)可以接收函數(shù)作為參數(shù),或者返回一個函數(shù)結果,或者兩者同時具備。
現(xiàn)在可以使用我們的高階函數(shù)來替代前的4個函數(shù)(這個在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個幾乎一模一樣的函數(shù)要好多了。
但是請注意正則表達式。它們有一點啰嗦。我們再重構一下:
<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>
好一點了?,F(xiàn)在當我們想要分析一個電話號碼時,我們不需要再復制粘貼正則表達式。
但是想象一下我們有更多的正則表達式要分析,不僅僅是parseSsn和parsePhone。每次我們創(chuàng)建一個正則表達式解析器,我們需要記住添加.exec到它的末尾。說真的,對我來說很容易忘記。
我們可以通過創(chuàng)建一個返回exec函數(shù)的高階函數(shù)來防止這種情況:
<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函數(shù),exec函數(shù)接收一個字符串參數(shù)。validateValueWithFunc函數(shù)會傳遞字符串(value)給解析函數(shù),即exec。
的確,這是一個微不足道的改進,但是這里展示了一個高階函數(shù)返回一個函數(shù)的例子。
但是,你可以想象一下如果makeRegexParser比現(xiàn)在要復雜得多的時候,這個改進所帶來的好處。
這里還有另外一個高階函數(shù)返回函數(shù)的例子:
function makeAdder(constantValue) {
return function adder(value) {
return constantValue + value;
};
}
makeAdder接收一個參數(shù)constantValue返回一個adder,adder是一個函數(shù),它將給它接收的參數(shù)加上一個常量值。
這是它怎么被使用:
var add10 = makeAdder(10);
console.log(add10(20)); // prints 30
console.log(add10(30)); // prints 40
console.log(add10(40)); // prints 50
我們通過向makeAdder函數(shù)(它會返回一個函數(shù))傳遞一個常量10來創(chuàng)建一個函數(shù)add10,add10將給任意值加上10。
注意adder函數(shù)可以訪問constantValue,即使makeAddr函數(shù)已經(jīng)返回。因為adder在創(chuàng)建時constantValue在它的作用域內(nèi)。
這個行為非常的重要,因為如果沒有它,能夠返回函數(shù)的函數(shù)不是很有用。所以我們理解它是怎么工作以及怎么稱呼它非常要。
這種行為成為閉包。
閉包(Closures)

這里有一個人為的使用閉包的函數(shù)例子:
<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函數(shù)。
類似的,childFunc保持child的作用域存活,因為parentFunc(這里就是parent)返回child函數(shù)。
當一個函數(shù)被創(chuàng)建,其整個生命周期中都是可以訪問在在其創(chuàng)建時作用域內(nèi)的所有變量。只要有引用指向它該函數(shù)就會一直存在。例如:只要childFunc還引用它,child的作用域就一直存在。
閉包就是一個函數(shù)的作用域,這個作用域通過指向該函數(shù)的引用保持存活。
注意在JavaScript中閉包是會造成問題的,因為這些變量是可修改的,即,從它們結束一直到他們返回的函數(shù)被調(diào)用期間,它們都可以修改變量。
幸虧,函數(shù)式語言中的變量都是不可修改的,這樣可以消除這個常見的bug以及困惑根源。
