「前端進階」完全弄懂函數柯里化

你知道的越多,你不知道的越多
點贊再看,手留餘香,與有榮焉javascript

前言

隨着主流JavaScript中函數式編程的迅速發展, 函數柯里化在許多應用程序中已經變得很廣泛。 瞭解它們是什麼,它們如何工做以及如何充分利用它們很是重要。前端

什麼是柯里化( curry)

在數學和計算機科學中,柯里化是一種將使用多個參數的一個函數轉換成一系列使用一個參數的函數的技術。java

舉例來講,一個接收3個參數的普通函數,在進行柯里化後, 柯里化版本的函數接收一個參數並返回接收下一個參數的函數, 該函數返回一個接收第三個參數的函數。 最後一個函數在接收第三個參數後, 將以前接收到的三個參數應用於原普通函數中,並返回最終結果。git

// 數學和計算科學中的柯里化:

//一個接收三個參數的普通函數
function sum(a,b,c) {
    console.log(a+b+c)
}

//用於將普通函數轉化爲柯里化版本的工具函數
function curry(fn) {
  //...內部實現省略,返回一個新函數
}

//獲取一個柯里化後的函數
let _sum = curry(sum);

//返回一個接收第二個參數的函數
let A = _sum(1);
//返回一個接收第三個參數的函數
let B = A(2);
//接收到最後一個參數,將以前全部的參數應用到原函數中,並運行
B(3)    // print : 6
複製代碼

而對於Javascript語言來講,咱們一般說的柯里化函數的概念,與數學和計算機科學中的柯里化的概念並不徹底同樣。github

在數學和計算機科學中的柯里化函數,一次只能傳遞一個參數;編程

而咱們Javascript實際應用中的柯里化函數,能夠傳遞一個或多個參數。數組

來看這個例子:微信

//普通函數
function fn(a,b,c,d,e) {
  console.log(a,b,c,d,e)
}
//生成的柯里化函數
let _fn = curry(fn);

_fn(1,2,3,4,5);     // print: 1,2,3,4,5
_fn(1)(2)(3,4,5);   // print: 1,2,3,4,5
_fn(1,2)(3,4)(5);   // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5
複製代碼

對於已經柯里化後的 _fn 函數來講,當接收的參數數量與原函數的形參數量相同時,執行原函數; 當接收的參數數量小於原函數的形參數量時,返回一個函數用於接收剩餘的參數,直至接收的參數數量與形參數量一致,執行原函數。markdown

當咱們知道柯里化是什麼了的時候,咱們來看看柯里化到底有什麼用?app

柯里化的用途

柯里化實際是把簡答的問題複雜化了,可是複雜化的同時,咱們在使用函數時擁有了更加多的自由度。 而這裏對於函數參數的自由處理,正是柯里化的核心所在。 柯里化本質上是下降通用性,提升適用性。來看一個例子:

咱們工做中會遇到各類須要經過正則檢驗的需求,好比校驗電話號碼、校驗郵箱、校驗身份證號、校驗密碼等, 這時咱們會封裝一個通用函數 checkByRegExp ,接收兩個參數,校驗的正則對象和待校驗的字符串

function checkByRegExp(regExp,string) {
    return regExp.test(string);  
}

checkByRegExp(/^1\d{10}$/, '18642838455'); // 校驗電話號碼
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com'); // 校驗郵箱
複製代碼

上面這段代碼,乍一看沒什麼問題,能夠知足咱們全部經過正則檢驗的需求。 可是咱們考慮這樣一個問題,若是咱們須要校驗多個電話號碼或者校驗多個郵箱呢?

咱們可能會這樣作:

checkByRegExp(/^1\d{10}$/, '18642838455'); // 校驗電話號碼
checkByRegExp(/^1\d{10}$/, '13109840560'); // 校驗電話號碼
checkByRegExp(/^1\d{10}$/, '13204061212'); // 校驗電話號碼

checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com'); // 校驗郵箱
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@qq.com'); // 校驗郵箱
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@gmail.com'); // 校驗郵箱
複製代碼

咱們每次進行校驗的時候都須要輸入一串正則,再校驗同一類型的數據時,相同的正則咱們須要寫屢次, 這就致使咱們在使用的時候效率低下,而且因爲 checkByRegExp 函數自己是一個工具函數並無任何意義, 一段時間後咱們從新來看這些代碼時,若是沒有註釋,咱們必須經過檢查正則的內容, 咱們才能知道咱們校驗的是電話號碼仍是郵箱,仍是別的什麼。

此時,咱們能夠藉助柯里化對 checkByRegExp 函數進行封裝,以簡化代碼書寫,提升代碼可讀性。

//進行柯里化
let _check = curry(checkByRegExp);
//生成工具函數,驗證電話號碼
let checkCellPhone = _check(/^1\d{10}$/);
//生成工具函數,驗證郵箱
let checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);

checkCellPhone('18642838455'); // 校驗電話號碼
checkCellPhone('13109840560'); // 校驗電話號碼
checkCellPhone('13204061212'); // 校驗電話號碼

checkEmail('test@163.com'); // 校驗郵箱
checkEmail('test@qq.com'); // 校驗郵箱
checkEmail('test@gmail.com'); // 校驗郵箱
複製代碼

再來看看經過柯里化封裝後,咱們的代碼是否是變得又簡潔又直觀了呢。

通過柯里化後,咱們生成了兩個函數 checkCellPhone 和 checkEmail, checkCellPhone 函數只能驗證傳入的字符串是不是電話號碼, checkEmail 函數只能驗證傳入的字符串是不是郵箱, 它們與 原函數 checkByRegExp 相比,從功能上通用性下降了,但適用性提高了。 柯里化的這種用途能夠被理解爲:參數複用

咱們再來看一個例子

假定咱們有這樣一段數據:

let list = [
    {
        name:'lucy'
    },
    {
        name:'jack'
    }
]
複製代碼

咱們須要獲取數據中的全部 name 屬性的值,常規思路下,咱們會這樣實現:

let names = list.map(function(item) {
  return item.name;
})
複製代碼

那麼咱們如何用柯里化的思惟來實現呢

let prop = curry(function(key,obj) {
    return obj[key];
})
let names = list.map(prop('name'))
複製代碼

看到這裏,可能會有疑問,這麼簡單的例子,僅僅只是爲了獲取 name 的屬性值,爲什麼還要實現一個 prop 函數呢,這樣太麻煩了吧。

咱們能夠換個思路,prop 函數實現一次後,之後是能夠屢次使用的,因此咱們在考慮代碼複雜程度的時候,是能夠將 prop 函數的實現去掉的。

咱們實際的代碼能夠理解爲只有一行 let names = list.map(prop('name'))

這麼看來,經過柯里化的方式,咱們的代碼是否是變得更精簡了,而且可讀性更高了呢。

如何封裝柯里化工具函數

接下來,咱們來思考如何實現 curry 函數。

回想以前咱們對於柯里化的定義,接收一部分參數,返回一個函數接收剩餘參數,接收足夠參數後,執行原函數。

咱們已經知道了,當柯里化函數接收到足夠參數後,就會執行原函數,那麼咱們如何去肯定什麼時候達到足夠的參數呢?

咱們有兩種思路:

  1. 經過函數的 length 屬性,獲取函數的形參個數,形參的個數就是所需的參數個數
  2. 在調用柯里化工具函數時,手動指定所需的參數個數

咱們將這兩點結合如下,實現一個簡單 curry 函數:

/** * 將函數柯里化 * @param fn 待柯里化的原函數 * @param len 所需的參數個數,默認爲原函數的形參個數 */
function curry(fn,len = fn.length) {
    return _curry.call(this,fn,len)
}

/** * 中轉函數 * @param fn 待柯里化的原函數 * @param len 所需的參數個數 * @param args 已接收的參數列表 */
function _curry(fn,len,...args) {
    return function (...params) {
        let _args = [...args,...params];
        if(_args.length >= len){
            return fn.apply(this,_args);
        }else{
            return _curry.call(this,fn,len,..._args)
        }
    }
}
複製代碼

咱們來驗證一下:

let _fn = curry(function(a,b,c,d,e){
    console.log(a,b,c,d,e)
});

_fn(1,2,3,4,5);     // print: 1,2,3,4,5
_fn(1)(2)(3,4,5);   // print: 1,2,3,4,5
_fn(1,2)(3,4)(5);   // print: 1,2,3,4,5
_fn(1)(2)(3)(4)(5); // print: 1,2,3,4,5
複製代碼

咱們經常使用的工具庫 lodash 也提供了 curry 方法,而且增長了很是好玩的 placeholder 功能,經過佔位符的方式來改變傳入參數的順序。

好比說,咱們傳入一個佔位符,本次調用傳遞的參數略過佔位符, 佔位符所在的位置由下次調用的參數來填充,好比這樣:

直接看一下官網的例子:

接下來咱們來思考,如何實現佔位符的功能。

對於 lodash 的 curry 函數來講,curry 函數掛載在 lodash 對象上,因此將 lodash 對象當作默認佔位符來使用。

而咱們的本身實現的 curry 函數,自己並無掛載在任何對象上,因此將 curry 函數當作默認佔位符

使用佔位符,目的是改變參數傳遞的順序,因此在 curry 函數實現中,每次須要記錄是否使用了佔位符,而且記錄佔位符所表明的參數位置。

直接上代碼:

/** * @param fn 待柯里化的函數 * @param length 須要的參數個數,默認爲函數的形參個數 * @param holder 佔位符,默認當前柯里化函數 * @return {Function} 柯里化後的函數 */
function curry(fn,length = fn.length,holder = curry){
    return _curry.call(this,fn,length,holder,[],[])
}
/** * 中轉函數 * @param fn 柯里化的原函數 * @param length 原函數須要的參數個數 * @param holder 接收的佔位符 * @param args 已接收的參數列表 * @param holders 已接收的佔位符位置列表 * @return {Function} 繼續柯里化的函數 或 最終結果 */
function _curry(fn,length,holder,args,holders){
    return function(..._args){
        //將參數複製一份,避免屢次操做同一函數致使參數混亂
        let params = args.slice();
        //將佔位符位置列表複製一份,新增長的佔位符增長至此
        let _holders = holders.slice();
        //循環入參,追加參數 或 替換佔位符
        _args.forEach((arg,i)=>{
            //真實參數 以前存在佔位符 將佔位符替換爲真實參數
            if (arg !== holder && holders.length) {
                let index = holders.shift();
                _holders.splice(_holders.indexOf(index),1);
                params[index] = arg;
            }
            //真實參數 以前不存在佔位符 將參數追加到參數列表中
            else if(arg !== holder && !holders.length){
                params.push(arg);
            }
            //傳入的是佔位符,以前不存在佔位符 記錄佔位符的位置
            else if(arg === holder && !holders.length){
                params.push(arg);
                _holders.push(params.length - 1);
            }
            //傳入的是佔位符,以前存在佔位符 刪除原佔位符位置
            else if(arg === holder && holders.length){
                holders.shift();
            }
        });
        // params 中前 length 條記錄中不包含佔位符,執行函數
        if(params.length >= length && params.slice(0,length).every(i=>i!==holder)){
            return fn.apply(this,params);
        }else{
            return _curry.call(this,fn,length,holder,params,_holders)
        }
    }
}

複製代碼

驗證一下:;

let fn = function(a, b, c, d, e) {
    console.log([a, b, c, d, e]);
}

let _ = {}; // 定義佔位符
let _fn = curry(fn,5,_);  // 將函數柯里化,指定所需的參數個數,指定所需的佔位符

_fn(1, 2, 3, 4, 5);                 // print: 1,2,3,4,5
_fn(_, 2, 3, 4, 5)(1);              // print: 1,2,3,4,5
_fn(1, _, 3, 4, 5)(2);              // print: 1,2,3,4,5
_fn(1, _, 3)(_, 4,_)(2)(5);         // print: 1,2,3,4,5
_fn(1, _, _, 4)(_, 3)(2)(5);        // print: 1,2,3,4,5
_fn(_, 2)(_, _, 4)(1)(3)(5);        // print: 1,2,3,4,5
複製代碼

至此,咱們已經完整實現了一個 curry 函數~~

系列文章推薦

寫在最後

  • 文中若有錯誤,歡迎在評論區指正,若是這篇文章幫到了你,歡迎點贊關注
  • 本文同步首發與github,可在github中找到更多精品文章,歡迎Watch & Star ★
  • 後續文章參見:計劃

歡迎關注微信公衆號【前端小黑屋】,每週1-3篇精品優質文章推送,助你走上進階之旅

相關文章
相關標籤/搜索