前端面試手寫代碼——函數(shù)柯里化

1 什么是函數(shù)柯里化

在計(jì)算機(jī)科學(xué)中,柯里化(Currying)是把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)(最初函數(shù)的第一個(gè)參數(shù))的函數(shù),并且返回接受余下的參數(shù)且返回結(jié)果的新函數(shù)的技術(shù)。這個(gè)技術(shù)以邏輯學(xué)家 Haskell Curry 命名的。

什么意思?簡(jiǎn)單來(lái)說(shuō),柯里化是一項(xiàng)技術(shù),它用來(lái)改造多參數(shù)的函數(shù)。比如:

// 這是一個(gè)接受3個(gè)參數(shù)的函數(shù)
const add = function(x, y, z) {
  return x + y + z
}

我們將它變換一下,可以得到這樣一個(gè)函數(shù):

// 接收一個(gè)單一參數(shù)
const curryingAdd = function(x) {
  // 并且返回接受余下的參數(shù)的函數(shù)
  return function(y, z) {
    return x + y + z
  }
}

這樣有什么區(qū)別呢?從調(diào)用上來(lái)對(duì)比:

// 調(diào)用add
add(1, 2, 3)

// 調(diào)用curryingAdd
curryingAdd(1)(2, 3)
// 看得更清楚一點(diǎn),等價(jià)于下面
const fn = curryingAdd(1)
fn(2, 3)

可以看到,變換后的的函數(shù)可以分批次接受參數(shù),先記住這一點(diǎn),下面會(huì)講用處。甚至fncurryingAdd返回的函數(shù))還可以繼續(xù)變換

const curryingAdd = function(x) {
  return function(y) {
    return function(z) {
      return x + y + z
    }
  }
}
// 調(diào)用
curryingAdd(1)(2)(3)
// 即
const fn = curryingAdd(1)
const fn1 = fn(2)
fn1(3)

上面的兩次變換過程,就是函數(shù)柯里化。

簡(jiǎn)單講就是把一個(gè)多參數(shù)的函數(shù)f,變換成接受部分參數(shù)的函數(shù)g,并且這個(gè)函數(shù)g會(huì)返回一個(gè)函數(shù)h,函數(shù)h用來(lái)接受其他參數(shù)。函數(shù)h可以繼續(xù)柯里化。就是一個(gè)套娃的過程~

那么費(fèi)這么大勁將函數(shù)柯里化有什么用呢?

2 柯里化的作用和特點(diǎn)

2.1 參數(shù)復(fù)用

工作中會(huì)遇到的需求:通過正則校驗(yàn)電話號(hào)、郵箱、身份證是否合法等等

于是我們會(huì)封裝一個(gè)校驗(yàn)函數(shù)如下:

/**
 * @description 通過正則校驗(yàn)字符串
 * @param {RegExp} regExp 正則對(duì)象
 * @param {String} str 待校驗(yàn)字符串
 * @return {Boolean} 是否通過校驗(yàn)
 */
function checkByRegExp(regExp, str) {
    return regExp.test(str)
}

假如我們要校驗(yàn)很多手機(jī)號(hào)、郵箱,我們就會(huì)這樣調(diào)用:

// 校驗(yàn)手機(jī)號(hào)
checkByRegExp(/^1\d{10}$/, '15152525634'); 
checkByRegExp(/^1\d{10}$/, '13456574566'); 
checkByRegExp(/^1\d{10}$/, '18123787385'); 
// 校驗(yàn)郵箱
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'fsds@163.com'); 
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'fdsf@qq.com'); 
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'fjks@qq.com');

貌似沒什么問題,事實(shí)上還有改進(jìn)的空間

  1. 校驗(yàn)同一類型的數(shù)據(jù)時(shí),相同的正則我們寫了很多次。
  2. 代碼可讀性較差,如果沒有注釋,我們并不能一下就看出來(lái)正則的作用

我們?cè)囍褂煤瘮?shù)柯里化來(lái)改進(jìn):

// 將函數(shù)柯里化
function checkByRegExp(regExp) {
    return function(str) {
        return regExp.test(str)
    }
}

于是我們傳入不同的正則對(duì)象,就可以得到功能不同的函數(shù):

// 校驗(yàn)手機(jī)
const checkPhone = curryingCheckByRegExp(/^1\d{10}$/)
// 校驗(yàn)郵箱
const checkEmail = curryingCheckByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/)

現(xiàn)在校驗(yàn)手機(jī)、郵箱的代碼就簡(jiǎn)單了,并且可讀性也增強(qiáng)了

// 校驗(yàn)手機(jī)號(hào)
checkPhone('15152525634'); 
checkPhone('13456574566'); 
checkPhone('18123787385'); 
// 校驗(yàn)郵箱
checkEmail('fsds@163.com'); 
checkEmail('fdsf@qq.com'); 
checkEmail('fjks@qq.com');

這就是參數(shù)復(fù)用:我們只需將第一個(gè)參數(shù)regExp復(fù)用,就可以直接調(diào)用有特定功能的函數(shù)

通用函數(shù)(如checkByRegExp)解決了兼容性問題,但也會(huì)帶來(lái)使用的不便,比如不同的應(yīng)用場(chǎng)景需要傳遞多個(gè)不同的參數(shù)來(lái)解決問題

有的時(shí)候同一種規(guī)則可能會(huì)反復(fù)使用(比如校驗(yàn)手機(jī)的參數(shù)),這就造成了代碼的重復(fù),利用柯里化就能夠消除重復(fù),達(dá)到復(fù)用參數(shù)的目的。

柯里化的一種重要思想:降低適用范圍,提高適用性

2.2 提前返回

在JS DOM事件監(jiān)聽程序中,我們用addEventListener方法為元素添加事件處理程序,但是部分瀏覽器版本不支持此方法,我們會(huì)使用attachEvent方法來(lái)替代。

這時(shí)我們會(huì)寫一個(gè)兼容各瀏覽器版本的代碼:

/**
 * @description: 
 * @param {object} element DOM元素對(duì)象
 * @param {string} type 事件類型
 * @param {Function} fn 事件處理函數(shù)
 * @param {boolean} isCapture 是否捕獲
 * @return {void}
 */
function addEvent(element, type, fn, isCapture) {
    if (window.addEventListener) {
        element.addEventListener(type, fn, isCapture)
    } else if (window.attachEvent) {
        element.attachEvent("on" + type, fn)
    }
}

我們用addEvent來(lái)添加事件監(jiān)聽,但是每次調(diào)用此方法時(shí),都會(huì)進(jìn)行一次判斷,事實(shí)上瀏覽器版本確定下來(lái)后,沒有必要進(jìn)行重復(fù)判斷。

柯里化處理:

function curryingAddEvent() {
    if (window.addEventListener) {
        return function(element, type, fn, isCapture) {
            element.addEventListener(type, fn, isCapture)
        }
    } else if (window.attachEvent) {
        return function(element, type, fn) {
            element.attachEvent("on" + type, fn)
        }
    }
}
const addEvent = curryingAddEvent()

// 也可以用立即執(zhí)行函數(shù)將上述代碼合并
const addEvent = (function curryingAddEvent() {
    ...
})()

現(xiàn)在我們得到的addEvent是經(jīng)過判斷后得到的函數(shù),以后調(diào)用就不用重復(fù)判斷了。

這就是提前返回或者說(shuō)提前確認(rèn),函數(shù)柯里化后可以提前處理部分任務(wù),返回一個(gè)函數(shù)處理其他任務(wù)

另外,我們可以看到,curryingAddEvent好像并沒有接受參數(shù)。這是因?yàn)樵瘮?shù)的條件(即瀏覽器的版本是否支持addEventListener)是直接從全局獲取的。邏輯上其實(shí)是可以改成:

let mode = window.addEventListener ? 0 : 1;
function addEvent(mode, element, type, fn, isCapture) {
  if (mode === 0) {
    element.addEventListener(type, fn, isCapture);
  } else if (mode === 1) {
    element.attachEvent("on" + type, fn);
  }
}
// 這樣柯里化后就可以先接受一個(gè)參數(shù)了
function curryingAddEvent(mode) {
    if (mode === 0) {
        return function(element, type, fn, isCapture) {
            element.addEventListener(type, fn, isCapture)
        }
    } else if (mode === 1) {
        return function(element, type, fn) {
            element.attachEvent("on" + type, fn)
        }
    }
}

當(dāng)然沒必要這么改~

2.3 延遲執(zhí)行

事實(shí)上,上述正則校驗(yàn)和事件監(jiān)聽的例子中已經(jīng)體現(xiàn)了延遲執(zhí)行。

curryingCheckByRegExp函數(shù)調(diào)用后返回了checkPhonecheckEmail函數(shù)

curringAddEvent函數(shù)調(diào)用后返回了addEvent函數(shù)

返回的函數(shù)都不會(huì)立即執(zhí)行,而是等待調(diào)用。

3 封裝通用柯里化工具函數(shù)

上面我們對(duì)函數(shù)進(jìn)行柯里化都是手動(dòng)修改了原函數(shù),將add改成了curryingAdd、將checkByRegExp改成了curryingCheckByRegExp、將addEvent改成了curryingAddEvent。

難道我們每次對(duì)函數(shù)進(jìn)行柯里化都要手動(dòng)修改底層函數(shù)嗎?當(dāng)然不是

我們可以封裝一個(gè)通用柯里化工具函數(shù)(面試手寫代碼)

/**
 * @description: 將函數(shù)柯里化的工具函數(shù)
 * @param {Function} fn 待柯里化的函數(shù)
 * @param {array} args 已經(jīng)接收的參數(shù)列表
 * @return {Function}
 */
const currying = function(fn, ...args) {
    // fn需要的參數(shù)個(gè)數(shù)
    const len = fn.length
    // 返回一個(gè)函數(shù)接收剩余參數(shù)
    return function (...params) {
        // 拼接已經(jīng)接收和新接收的參數(shù)列表
        let _args = [...args, ...params]
        // 如果已經(jīng)接收的參數(shù)個(gè)數(shù)還不夠,繼續(xù)返回一個(gè)新函數(shù)接收剩余參數(shù)
        if (_args.length < len) {
            return currying.call(this, fn, ..._args)
        }
        // 參數(shù)全部接收完調(diào)用原函數(shù)
        return fn.apply(this, _args)
    }
}

這個(gè)柯里化工具函數(shù)用來(lái)接收部分參數(shù),然后返回一個(gè)新函數(shù)等待接收剩余參數(shù),遞歸直到接收到全部所需參數(shù),然后通過apply調(diào)用原函數(shù)。

現(xiàn)在我們基本不用手動(dòng)修改原函數(shù)來(lái)將函數(shù)柯里化了

// 直接用工具函數(shù)返回校驗(yàn)手機(jī)、郵箱的函數(shù)
const checkPhone = currying(checkByRegExp(/^1\d{10}$/))
const checkEmail = currying(checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/))

但是上面事件監(jiān)聽的例子就不能用這個(gè)工具函數(shù)進(jìn)行柯里化了,原因前面說(shuō)了,因?yàn)樗臈l件直接從全局獲取了,所以比較特殊,改成從外部傳入條件,就能用工具函數(shù)柯里化了。當(dāng)然沒這個(gè)必要,直接修改原函數(shù)更直接、可讀性更強(qiáng)

4 總結(jié)和補(bǔ)充

  1. 柯里化突出一種重要思想:降低適用范圍,提高適用性
  2. 柯里化的三個(gè)作用和特點(diǎn):參數(shù)復(fù)用、提前返回、延遲執(zhí)行
  3. 柯里化是閉包的一個(gè)典型應(yīng)用,利用閉包形成了一個(gè)保存在內(nèi)存中的作用域,把接收到的部分參數(shù)保存在這個(gè)作用域中,等待后續(xù)使用。并且返回一個(gè)新函數(shù)接收剩余參數(shù)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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