JavaScript函數柯裡化
1 什麼是函數柯裡化
在計算機科學中,柯裡化(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可以繼續柯裡化。就是一個套娃的過程~
那麼費這麼大勁將函數柯裡化有什麼用呢?
2 柯裡化的作用和特點
2.1 參數復用
工作中會遇到的需求:通過正則校驗電話號、郵箱、身份證是否合法等等
於是我們會封裝一個校驗函數如下:
/** * @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]');
貌似沒什麼問題,事實上還有改進的空間
- 校驗同一類型的數據時,相同的正則我們寫瞭很多次。
- 代碼可讀性較差,如果沒有註釋,我們並不能一下就看出來正則的作用
我們試著使用函數柯裡化來改進:
// 將函數柯裡化 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
)解決瞭兼容性問題,但也會帶來使用的不便,比如不同的應用場景需要傳遞多個不同的參數來解決問題
有的時候同一種規則可能會反復使用(比如校驗手機的參數),這就造成瞭代碼的重復,利用柯裡化就能夠消除重復,達到復用參數的目的。
柯裡化的一種重要思想:降低適用范圍,提高適用性
2.2 提前返回
在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) } } }
當然沒必要這麼改~
2.3 延遲執行
事實上,上述正則校驗和事件監聽的例子中已經體現瞭延遲執行。
curryingCheckByRegExp
函數調用後返回瞭checkPhone
和checkEmail
函數
curringAddEvent
函數調用後返回瞭addEvent
函數
返回的函數都不會立即執行,而是等待調用。
3 封裝通用柯裡化工具函數#
上面我們對函數進行柯裡化都是手動修改瞭原函數,將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+)+)$/))
但是上面事件監聽的例子就不能用這個工具函數進行柯裡化瞭,原因前面說瞭,因為它的條件直接從全局獲取瞭,所以比較特殊,改成從外部傳入條件,就能用工具函數柯裡化瞭。當然沒這個必要,直接修改原函數更直接、可讀性更強
4 總結和補充
- 柯裡化突出一種重要思想:降低適用范圍,提高適用性
- 柯裡化的三個作用和特點:參數復用、提前返回、延遲執行
- 柯裡化是閉包的一個典型應用,利用閉包形成瞭一個保存在內存中的作用域,把接收到的部分參數保存在這個作用域中,等待後續使用。並且返回一個新函數接收剩餘參數
到此這篇關於JavaScript
函數柯裡化的文章就介紹到這瞭,更多相關函數柯裡化內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- js事件流、事件委托與事件階段實例詳解
- Vue中addEventListener() 監聽事件案例講解
- 詳細聊聊閉包在js中充當著什麼角色
- 如何用JavaScript讓你的瀏覽器說話
- 詳解前端安全之JavaScript防http劫持與XSS