JavaScript函數(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
命名的。
什么意思?簡單來說,柯里化是一項(xiàng)技術(shù),它用來改造多參數(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)用上來對比:
// 調(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),下面會講用處。甚至fn(curryingAdd
返回的函數(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ù)柯里化。
簡單講就是把一個(gè)多參數(shù)的函數(shù)f
,變換成接受部分參數(shù)的函數(shù)g
,并且這個(gè)函數(shù)g
會返回一個(gè)函數(shù)h
,函數(shù)h用來接受其他參數(shù)。函數(shù)h可以繼續(xù)柯里化。就是一個(gè)套娃的過程~
那么費(fèi)這么大勁將函數(shù)柯里化有什么用呢?
2 柯里化的作用和特點(diǎn)
2.1 參數(shù)復(fù)用
工作中會遇到的需求:通過正則校驗(yàn)電話號、郵箱、身份證是否合法等等
于是我們會封裝一個(gè)校驗(yàn)函數(shù)如下:
/** * @description 通過正則校驗(yàn)字符串 * @param {RegExp} regExp 正則對象 * @param {String} str 待校驗(yàn)字符串 * @return {Boolean} 是否通過校驗(yàn) */ function checkByRegExp(regExp, str) { return regExp.test(str) }
假如我們要校驗(yàn)很多手機(jī)號、郵箱,我們就會這樣調(diào)用:
// 校驗(yàn)手機(jī)號 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)的空間
- 校驗(yàn)同一類型的數(shù)據(jù)時(shí),相同的正則我們寫了很多次。
- 代碼可讀性較差,如果沒有注釋,我們并不能一下就看出來正則的作用
我們試著使用函數(shù)柯里化來改進(jìn):
// 將函數(shù)柯里化 function checkByRegExp(regExp) { return function(str) { return regExp.test(str) } }
于是我們傳入不同的正則對象,就可以得到功能不同的函數(shù):
// 校驗(yàn)手機(jī) const checkPhone = curryingCheckByRegExp(/^1\d{10}$/) // 校驗(yàn)郵箱 const checkEmail = curryingCheckByRegExp(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/)
現(xiàn)在校驗(yàn)手機(jī)、郵箱的代碼就簡單了,并且可讀性也增強(qiáng)了
// 校驗(yàn)手機(jī)號 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
)解決了兼容性問題,但也會帶來使用的不便,比如不同的應(yīng)用場景需要傳遞多個(gè)不同的參數(shù)來解決問題
有的時(shí)候同一種規(guī)則可能會反復(fù)使用(比如校驗(yàn)手機(jī)的參數(shù)),這就造成了代碼的重復(fù),利用柯里化就能夠消除重復(fù),達(dá)到復(fù)用參數(shù)的目的。
柯里化的一種重要思想:降低適用范圍,提高適用性
2.2 提前返回
在JS DOM
事件監(jiān)聽程序中,我們用addEventListener
方法為元素添加事件處理程序,但是部分瀏覽器版本不支持此方法,我們會使用attachEvent
方法來替代。
這時(shí)我們會寫一個(gè)兼容各瀏覽器版本的代碼:
/** * @description: * @param {object} element DOM元素對象 * @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
來添加事件監(jiān)聽,但是每次調(diào)用此方法時(shí),都會進(jìn)行一次判斷,事實(shí)上瀏覽器版本確定下來后,沒有必要進(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ù)判斷了。
這就是提前返回或者說提前確認(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)用后返回了checkPhone
和checkEmail
函數(shù)
curringAddEvent
函數(shù)調(diào)用后返回了addEvent
函數(shù)
返回的函數(shù)都不會立即執(zhí)行,而是等待調(diào)用。
3 封裝通用柯里化工具函數(shù)#
上面我們對函數(shù)進(jìn)行柯里化都是手動修改了原函數(shù),將add
改成了curryingAdd
、將checkByRegExp
改成了curryingCheckByRegExp
、將addEvent
改成了curryingAddEvent
。
難道我們每次對函數(shù)進(jìn)行柯里化都要手動修改底層函數(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ù)用來接收部分參數(shù),然后返回一個(gè)新函數(shù)等待接收剩余參數(shù),遞歸直到接收到全部所需參數(shù),然后通過apply
調(diào)用原函數(shù)。
現(xiàn)在我們基本不用手動修改原函數(shù)來將函數(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)行柯里化了,原因前面說了,因?yàn)樗臈l件直接從全局獲取了,所以比較特殊,改成從外部傳入條件,就能用工具函數(shù)柯里化了。當(dāng)然沒這個(gè)必要,直接修改原函數(shù)更直接、可讀性更強(qiáng)
4 總結(jié)和補(bǔ)充
- 柯里化突出一種重要思想:降低適用范圍,提高適用性
- 柯里化的三個(gè)作用和特點(diǎn):參數(shù)復(fù)用、提前返回、延遲執(zhí)行
- 柯里化是閉包的一個(gè)典型應(yīng)用,利用閉包形成了一個(gè)保存在內(nèi)存中的作用域,把接收到的部分參數(shù)保存在這個(gè)作用域中,等待后續(xù)使用。并且返回一個(gè)新函數(shù)接收剩余參數(shù)
到此這篇關(guān)于JavaScript
函數(shù)柯里化的文章就介紹到這了,更多相關(guān)函數(shù)柯里化內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript嚴(yán)格模式不支持八進(jìn)制的問題講解
這篇文章主要講解JavaScript嚴(yán)格模式不支持八進(jìn)制的問題,本文圍繞JavaScript嚴(yán)格模式展開內(nèi)容,詳細(xì)介紹為什么JavaScript嚴(yán)格模式不支持八進(jìn)制,下面來看看詳細(xì)介紹,需要的朋友可以參考一下2021-11-11ECMAScript 6對象的擴(kuò)展實(shí)現(xiàn)示例
這篇文章主要為大家介紹了ECMAScript 6對象的擴(kuò)展實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08前端項(xiàng)目中監(jiān)聽localStorage的變化
這篇文章主要為大家介紹了前端項(xiàng)目中監(jiān)聽localStorage的變化的解決思路詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06微信小程序 兩種滑動方式(橫向滑動,豎向滑動)詳細(xì)及實(shí)例代碼
這篇文章主要介紹了微信小程序 兩種滑動方式詳細(xì)及實(shí)例代碼的相關(guān)資料,這里對橫向滑動和豎向滑動都做介紹,需要的朋友可以參考下2017-01-01umi插件開發(fā)仿dumi項(xiàng)目實(shí)現(xiàn)基礎(chǔ)路由解析
這篇文章主要為大家介紹了umi插件開發(fā)仿dumi項(xiàng)目實(shí)現(xiàn)基礎(chǔ)路由解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01微信小程序getPhoneNumber獲取用戶手機(jī)號
這篇文章主要介紹了 微信小程序getPhoneNumber獲取用戶手機(jī)號的相關(guān)資料,希望通過本文能幫助到大家,需要的朋友可以參考下2017-09-09