深入理解javascript系列(十七):函式柯里化

Panthon發表於2018-06-19

之前的系列,我們介紹了什麼是高階函式。所有以函式作為引數的函式,都可以叫作高階函式。並且我們常常利用高階函式來封裝一些公共邏輯。

本次,我們要繼續學習,繼續記錄,柯里化。柯里化,其實就是高階函式的一種特殊用法。

柯里化是指這樣一個函式(假設叫做createCurry),它接收函式A作為引數,執行後能夠返回一個新的函式,並且這個新的函式能夠處理函式A的剩餘引數。

文字總是不那麼好去理解,下面我們就通過例子來理解吧。

假設有一個接收三個引數的函式A。

function A(a, b, c) {
    // to do something
}複製程式碼

又假設我們有一個已經封裝好了的柯里化通用函式createCurry。他接收bar作為引數,能夠將A轉化為柯里化函式,返回結果就是這個被轉化之後的函式。

var _A = createCurry(A);複製程式碼

那麼_A作為createCurry執行的返回函式,能夠處理A的剩餘引數。因此下面的執行結果都是等價的。

_A(1, 2, 3);
_A(1,2)(3);
_A(1)(2,3);
_A(1)(2)(3);
A(1,2,3);複製程式碼

函式A被createCurry轉化之後得到柯里化函式_A,_A能夠處理A的所有剩餘引數。因此柯里化也被稱為部分求值。

在簡單的場景下,我們可以不借助柯里化通用式來轉化得到柯里化函式,僅憑藉眼力自己封裝。

例如,有一個簡單的加法函式,它能夠將自身的三個引數加起來並返回計算結果。

function add(a, b, c) {
    return a + b + c;
}複製程式碼

那麼add函式的柯里化函式_add則可以寫成:

function _add(a) {
    return function(b) {
        return function(c) {
            return a + b + c;
        }
    }
}複製程式碼

因此下面的運算方式是等價的。

add(1, 2, 3);
_add(1)(2)(3);複製程式碼

當然,柯里化通用式具備更加強大的能力,僅靠眼力勁可不行。因此我們更需要知道如何封裝這樣一個柯里化的通用式。

首先通過_add可以看出,柯里化函式的執行過程其實是一個引數收集過程,我們將每一次傳入的引數收集起來,並在最裡層進行處理。因此在實現createCurry時,可以藉助這個思路來進行封裝。

程式碼如下:

// arity 用來標記剩餘引數的個數
// args 用來收集引數

function createCurry(func, arity, args) {
    //第一次執行時,並不會傳入arity,而是直接獲取func引數的個數 func.length

    var arity = arity || func.length;
    
    //第一次執行也不會傳入args,而是預設為空陣列
    var args = args || [];
    
    var wrapper = function() {
        
        //將wrapper中的引數收集到args中
        var _args = [].slice.call(arguments);
        [].push.apply(args, _args);


        //如果引數個數小於最初的func.length,則遞迴呼叫,繼續收集引數
        if(_args.length < arity) {
            arity -= _args.length;
            return createCurry(func, arity, args);
        }

        //引數收集完畢,執行func
        return func.apply(func, args);
    }

    return wrapper;
}複製程式碼

是不是有些不太容易理解,所以要多閱讀幾次。這個createCurry的封裝其實是藉助了閉包和遞迴,實現一個引數收集,並在收集完畢之後執行所有引數。

不知道您是否有發現,函式經過createCurry轉化為一個柯里化函式後,最後執行的結果,不是正相當於執行函式自己嗎?柯里化是不是把簡單的問題複雜化了?

沒錯,柯里化確實是把簡單的問題複雜化了,但在複雜化的同時,我們在使用函式時擁有了更多的自由度。對於函式引數的自由處理,正是柯里化的核心所在。

下面舉一個常見的例子。

如果想要驗證一串數字是否是正確的手機號,那麼按照正常思路來做,可能就會寫出程式碼如下唉:

fuction checkPhone(phoneNumber) {
    return /^1[34578]\d{9}$/.test(phoneNumber);
}複製程式碼

而如果想要驗證是否是郵箱呢?你然後在寫一個,可是我們還會遇到更多需要驗證的訊息,如“身份證、登入名、密碼...”。為了偷懶,我們應該封裝一個更為通用的函式,把待驗證的正規表示式與將要被驗證的字串作為引數傳入:

function check(reg, targetString) {
    return reg.test(targetSting);
}複製程式碼

但是這樣封裝之後,在使用時又會遇到問題,因為總是需要輸入一串正則,一串字元,這樣就導致使用時效率低下。

這個時候,我們就可以藉助柯里化,在check的基礎上再做一層封裝,以簡化使用。

var _check = createCurry(check);

var checkPhone = _check(/xxxxxx/);
var checkEmail = _check(/xxxxxx/);複製程式碼

最後在使用時就會變得更加簡潔與直觀了。

checkPhone('13979227922');
checkEmail('xsxsx@163.com');複製程式碼

在這個過程中可以發現,柯里化能夠應對更加複雜的邏輯封裝。當情況變得多變時,柯里化依然能夠應付自如。

雖然柯里化在一定程度上將問題複雜化,也讓程式碼變得更加不容易理解,但是柯里化在面對複雜情況時的靈活性卻讓我們不得不愛。



相關文章