原文連接javascript
函數柯里化(Currying)是把接受多個參數的函數變成接受一個單一參數(最初的第一個參數)的函數,而且返回接受餘下的參數且返回結果的新函數的技術。前端
函數柯里化並非JavaScript特有的。用籠統的話形容則是:減小函數參數個數的傳遞,並返回一個新的函數,這個新的函數可以處理舊函數剩下的參數。java
簡單示例:git
// 計算兩個數相加並返回計算結果的函數,接受兩個參數a和b
function add (a, b) {
return a + b;
}
// 將函數柯里化
function curry (a) {
return function (b) {
return a + b;
}
}
// 應用函數柯里化
var _add = curry(1);
// 輸出結果
console.log(_add(2)); // 3
// 比較結果
console.log(_add(2) === add(1, 2)); // true
// 另外一種比較
console.log(curry(1)(2) === add(1, 2)); // true
複製代碼
這個是比較簡單的函數柯里化過程,細心的同窗會發現,示例中的函數封裝(柯里化)方式是具備較大的侷限性的,不過它能給你們對函數柯里化有一種大概的認識。github
以上簡單的示例可能並不能說什麼,接下來,咱們將給出更詳細的例子去配合理解。面試
詳細示例:數組
假設有一個接受三個參數且求三個數之和的add
函數閉包
function add (a, b, c) {
// do something...
}
複製代碼
而後通過咱們的柯里化(curry)函數封裝後獲得_add
函數app
var _add = curry(add);
複製代碼
那麼_add
是curry
封裝後返回的柯里化函數,根據上述的定義,它可以處理add
的剩餘參數。所以下面的函數調用都是等價的。函數
add(a, b, c) <=> _add(a, b, c) <=> _add(a, b)(c) <=> _add(a)(b,c) <=> _add(a)(b)(c)
複製代碼
因此說,柯里化也叫作"部分求值"。
咱們將上面簡單示例中的curry
函數,改爲更加通用形式:
function curry(fn) {
// 記錄原函數參數個數
var len= fn.length;
// 記錄傳參個數
var args = [].slice.call(arguments, 1);
// 返回新的函數
return function() {
// 保存新接收的參數爲數組
var _args = [].slice.call(arguments);
// 將新舊兩參數數組合並
[].unshift.apply(_args, args);
// 若是累積接收的參數個數少於原函數接受的參數,則遞歸調用
if (_args.length < len) {
return curry.call(this, fn, ..._args);
}
// 若個數知足要求,則返回原函數調用結果
return fn.apply(this, _args);
}
}
複製代碼
示例應用:
function add (a, b, c) {
console.log(a + b + c)
return a + b + c;
}
var _add = curry(add);
_add(1, 2, 3); // 6
_add(1)(2, 3); // 6
_add(1, 2)(3); // 6
_add(1)(2)(3); // 6
var _add = curry(add, 1);
_add(2, 3); // 6
_add(2)(3); // 6
var _add = curry(add, 1, 2);
_add(3); // 6
var _add = curry(add, 1, 2, 3);
_add(); // 6
複製代碼
這裏代碼邏輯也不難。咱們只需判斷參數個數是否達到函數柯里化前的個數,若沒有,則遞歸調用柯里化函數;若達到了,則執行函數,並返回執行後的結果。
有的同窗就苦惱了,函數柯里化,其實都最後還不是函數執行自身嗎,爲何還搞那麼多花裏胡哨的騷操做呢?函數柯里化確實把問題複雜化了,但同時提升了函數調用的自由度,這正是函數柯里化的核心所在。
請看一個常見的例子。
假設咱們有一個需求,須要驗證用戶輸入是不是正確的手機號碼,那麼你們可能會這樣封裝函數:
function checkPhone (phoneNumber) {
return /^1[34578]\d{9}$/.test(phoneNumber);
}
複製代碼
又假設咱們還有一個需求須要驗證郵箱正確性,咱們可能又有以下封裝:
function checkEmail(email) {
return /^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/.test(email);
}
複製代碼
這時候,產品經理又過來問咱們,能不能加上驗證身份證號碼、登錄密碼之類的。所以,咱們爲了保持通用性,經常會有這樣的封裝:
function check (reg, str) {
return reg.test(str);
}
複製代碼
這時,咱們就會有這樣子的調用形式:
check(/^1[34578]\d{9}$/, '12345678910');
check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'checkson@gmail.com');
...
複製代碼
若是要按照這種封裝形式,咱們要調用屢次驗證的話,須要屢次傳入相同的正則匹配,而正則匹配每每是固定不變的。那麼這個時候,咱們能夠經過函數柯里化來讓這些函數調用,變得優雅一些:
var _check = curry(check);
var checkPhone = _check(/^1[34578]\d{9}$/);
var checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
複製代碼
最後的函數調用就會變得簡潔明瞭了:
checkPhone('13912345678');
checkEmail('checkson@gmail.com');
複製代碼
咱們能夠發現,函數柯里化可以應對較複雜多變的業務需求,學好它是前端進階的重點。
前端有一道關於柯里化的面試題,廣爲流傳。
題目: 實現一個add方法,使如下等式成立
add(1)(2)(3) = 6
add(1, 2)(3)(4, 5) = 15
add(1, 2, 3)(4, 5)(6) = 21
add(1, 2) = 3
複製代碼
這裏須要補充一個重要的知識點:函數的隱式轉換。當咱們直接將函數參與其餘運算的時候,函數會默認調用toString
方法:
function fn () { return 1; }
console.log(fn + 1); // 輸出結果爲:function fn () { return 1; }1
複製代碼
咱們能夠重寫函數的toString
方法。
function fn() { return 1; }
fn.toString = function() { return 2; }
console.log(fn + 1); // 3
複製代碼
此外咱們還能夠重寫函數的valueOf
方法,獲得一樣的效果:
function fn() { return 1; }
fn.valueOf = function() { return 3; }
console.log(fn + 1); // 4
複製代碼
當同時重寫函數的toString
和valueOf
方法時,以valueOf
爲準。
function fn() { return 1; }
fn.toString = function() { return 2; }
fn.valueOf = function() { return 3; }
console.log(fn + 1); // 4
複製代碼
補充這個重要的知識點後,那麼我們直接擼代碼了:
function add () {
// 存儲全部參數
var args = [].slice.call(arguments);
function adder () {
// 保存參數
args.push(...arguments);
// 重寫valueOf方法
adder.valueOf = function () {
return args.reduce((a, b) => a + b);
}
// 遞歸返回adder函數
return adder;
}
// 返回adder函數調用
return adder();
}
複製代碼
代碼校驗:
console.log(add(1)(2)(3) == 6) // true
console.log(add(1, 2)(3)(4, 5) == 15) // true
console.log(add(1, 2, 3)(4, 5)(6) == 21) // true
console.log(add(1, 2) == 3) // true
複製代碼
這裏代碼的核心思想就是利用閉包來保存傳入的全部參數和函數隱式轉換。