成為一名函數(shù)式碼農(nóng)(2)

上一篇成為一名函數(shù)式碼農(nóng)(1)

友情提示

請仔細閱讀文中的代碼。確保你已經(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ù)處理valueregular expression和打印的message(至少打印消息的最后一部分)。

重構后的代碼:

<pre>
function validateValue(value, regex, type) {
if (regex.exec(value))
console.log('Invalid ' + type);
else
console.log('Valid ' + type);
}
</pre>

value代表之前代碼中的ssnphone。

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>

在這parseAddressparseFullName函數(shù)都是接受一個string,如果傳入的字符串符合預訂的句法則返回true。

我們怎么來重構這段代碼?

好的,我們可以像前面一樣使用value來代表addressname,使用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)在當我們想要分析一個電話號碼時,我們不需要再復制粘貼正則表達式。

但是想象一下我們有更多的正則表達式要分析,不僅僅是parseSsnparsePhone。每次我們創(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以及困惑根源。

下一篇:成為一名函數(shù)式碼農(nóng)(3)

本文譯自So You Want to be a Functional Programmer (Part 2)

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容