目錄
- 1 什么是函式柯里化
- 2 柯里化的作用和特點
- 2.1 引數復用
- 2.2 提前回傳
- 2.3 延遲執行
- 3 封裝通用柯里化工具函式
- 4 總結和補充
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 總結和補充
- 柯里化突出一種重要思想:降低適用范圍,提高適用性
- 柯里化的三個作用和特點:引數復用、提前回傳、延遲執行
- 柯里化是閉包的一個典型應用,利用閉包形成了一個保存在記憶體中的作用域,把接收到的部分引數保存在這個作用域中,等待后續使用,并且回傳一個新函式接收剩余引數
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/344510.html
標籤:其他
