天天看點

面試官:什麼是函數柯裡化?能手寫實作嗎?

1 什麼是函數柯裡化

2 柯裡化的作用和特點

2.1 參數複用

2.2 提前傳回

2.3 延遲執行

3 封裝通用柯裡化工具函數

4 總結和補充

目錄

    • 2.1 參數複用
    • 2.2 提前傳回
    • 2.3 延遲執行

在計算機科學中,柯裡化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,并且傳回接受餘下的參數且傳回結果的新函數的技術。這個技術以邏輯學家 Haskell Curry 命名的。

什麼意思?簡單來說,柯裡化是一項技術,它用來改造多參數的函數。比如:

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

我們将它變換一下,可以得到這樣一個函數:

// 接收一個單一參數
const curryingAdd = function(x) {
  // 并且傳回接受餘下的參數的函數
  return function(y, z) {
    return x + y + z
  }
}
           

這樣有什麼差別呢?從調用上來對比:

// 調用add
add(1, 2, 3)

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

可以看到,變換後的的函數可以分批次接受參數,先記住這一點,下面會講用處。甚至

fn

curryingAdd

傳回的函數)還可以繼續變換:

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

上面的兩次變換過程,就是函數柯裡化。

簡單講就是把一個多參數的函數

f

,變換成接受部分參數的函數

g

,并且這個函數

g

會傳回一個函數

h

,函數

h

用來接受其他參數。函數

h

可以繼續柯裡化。就是一個套娃的過程~

那麼費這麼大勁将函數柯裡化有什麼用呢?

工作中會遇到的需求:通過正則校驗電話号、郵箱、身份證是否合法等等

于是我們會封裝一個校驗函數如下:

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

假如我們要校驗很多手機号、郵箱,我們就會這樣調用:

// 校驗手機号
checkByRegExp(/^1\d{10}$/, '15152525634'); 
checkByRegExp(/^1\d{10}$/, '13456574566'); 
checkByRegExp(/^1\d{10}$/, '18123787385'); 
// 校驗郵箱
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, '[email protected]'); 
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, '[email protected]'); 
checkByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, '[email protected]');
           

貌似沒什麼問題,事實上還有改進的空間

  1. 校驗同一類型的資料時,相同的正則我們寫了很多次。
  2. 代碼可讀性較差,如果沒有注釋,我們并不能一下就看出來正則的作用

我們試着使用函數柯裡化來改進:

// 将函數柯裡化
function checkByRegExp(regExp) {
    return function(str) {
        return regExp.test(str)
    }
}
           

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

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

現在校驗手機、郵箱的代碼就簡單了,并且可讀性也增強了

// 校驗手機号
checkPhone('15152525634'); 
checkPhone('13456574566'); 
checkPhone('18123787385'); 
// 校驗郵箱
checkEmail('[email protected]'); 
checkEmail('[email protected]'); 
checkEmail('[email protected]');
           

這就是

參數複用

:我們隻需将第一個參數

regExp

複用,就可以直接調用有特定功能的函數

通用函數

(如

checkByRegExp

)解決了相容性問題,但也會帶來使用的不便,比如不同的應用場景需要傳遞多個不同的參數來解決問題

有的時候同一種規則可能會反複使用(比如校驗手機的參數),這就造成了代碼的重複,利用柯裡化就能夠消除重複,達到複用參數的目的。

柯裡化的一種重要思想:降低适用範圍,提高适用性

在JS DOM事件監聽程式中,我們用

addEventListener

方法為元素添加事件處理程式,但是部分浏覽器版本不支援此方法,我們會使用

attachEvent

方法來替代。

這時我們會寫一個相容各浏覽器版本的代碼:

/**
 * @description: 
 * @param {object} element DOM元素對象
 * @param {string} type 事件類型
 * @param {Function} fn 事件處理函數
 * @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

來添加事件監聽,但是每次調用此方法時,都會進行一次判斷,事實上浏覽器版本确定下來後,沒有必要進行重複判斷。

柯裡化處理:

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()

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

現在我們得到的

addEvent

是經過判斷後得到的函數,以後調用就不用重複判斷了。

提前傳回

或者說

提前确認

,函數柯裡化後可以提前處理部分任務,傳回一個函數處理其他任務

另外,我們可以看到,

curryingAddEvent

好像并沒有接受參數。這是因為原函數的條件(即浏覽器的版本是否支援

addEventListener

)是直接從全局擷取的。邏輯上其實是可以改成:

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);
  }
}
// 這樣柯裡化後就可以先接受一個參數了
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)
        }
    }
}
           

當然沒必要這麼改~

事實上,上述正則校驗和事件監聽的例子中已經展現了延遲執行。

curryingCheckByRegExp

函數調用後傳回了

checkPhone

checkEmail

函數

curringAddEvent

addEvent

傳回的函數都不會立即執行,而是等待調用。

上面我們對函數進行柯裡化都是手動修改了原函數,将

add

改成了

curryingAdd

、将

checkByRegExp

curryingCheckByRegExp

addEvent

curryingAddEvent

難道我們每次對函數進行柯裡化都要手動修改底層函數嗎?當然不是

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

/**
 * @description: 将函數柯裡化的工具函數
 * @param {Function} fn 待柯裡化的函數
 * @param {array} args 已經接收的參數清單
 * @return {Function}
 */
const currying = function(fn, ...args) {
    // fn需要的參數個數
    const len = fn.length
    // 傳回一個函數接收剩餘參數
    return function (...params) {
        // 拼接已經接收和新接收的參數清單
        let _args = [...args, ...params]
        // 如果已經接收的參數個數還不夠,繼續傳回一個新函數接收剩餘參數
        if (_args.length < len) {
            return currying.call(this, fn, ..._args)
        }
      	// 參數全部接收完調用原函數
        return fn.apply(this, _args)
    }
}
           

這個柯裡化工具函數用來接收部分參數,然後傳回一個新函數等待接收剩餘參數,遞歸直到接收到全部所需參數,然後通過

apply

調用原函數。

現在我們基本不用手動修改原函數來将函數柯裡化了

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

但是上面事件監聽的例子就不能用這個工具函數進行柯裡化了,原因前面說了,因為它的條件直接從全局擷取了,是以比較特殊,改成從外部傳入條件,就能用工具函數柯裡化了。當然沒這個必要,直接修改原函數更直接、可讀性更強

  1. 柯裡化突出一種重要思想:降低适用範圍,提高适用性
  2. 柯裡化的三個作用和特點:參數複用、提前傳回、延遲執行
  3. 柯裡化是閉包的一個典型應用,利用閉包形成了一個儲存在記憶體中的作用域,把接收到的部分參數儲存在這個作用域中,等待後續使用。并且傳回一個新函數接收剩餘參數

繼續閱讀