在Lambda演算(一套數理邏輯的形式系統,具體我也沒深刻研究過)中有個小技巧:假如一個函數只能收一個參數,那麼這個函數怎麼實現加法呢,由於高階函數是能夠當參數傳遞和返回值的,因此問題就簡化爲:寫一個只有一個參數的函數,而這個函數返回一個帶參數的函數,這樣就實現了能寫兩個參數的函數了(具體參見下邊代碼)——這就是所謂的柯里化(Currying,以邏輯學家Hsakell Curry命名),也能夠理解爲一種在處理函數過程當中的邏輯思惟方式。數組
function add(a, b) { return a + b; } //函數只能傳一個參數時候實現加法 function curry(a) { return function(b) { return a + b; } } var add2 = curry(2); //add2也就是第一個參數爲2的add版本 console.log(add2(3))//5
經過以上簡單介紹咱們大概瞭解了,函數柯里化基本是在作這麼一件事情:只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。用公式表示就是咱們要作的事情實際上是閉包
fn(a,b,c,d)=>fn(a)(b)(c)(d);app
fn(a,b,c,d)=>fn(a,b)(c)(d);函數
fn(a,b,c,d)=>fn(a)(b,c,d);this
......spa
再或者這樣:rest
fn(a,b,c,d)=>fn(a)(b)(c)(d)();code
fn(a,b,c,d)=>fn(a);fn(b);fn(c);fn(d);fn();blog
但不是這樣:io
fn(a,b,c,d)=>fn(a);
fn(a,b,c,d)=>fn(a,b);
......
這類不屬於柯里化內容,它也有個專業的名字叫偏函數,這個以後咱們也會提到。
下面咱們繼續把以前的add改成通用版本:
const curry = (fn, ...arg) => { let all = arg; return (...rest) => { all.push(...rest); return fn.apply(null, all); } } let add2 = curry(add, 2) console.log(add2(8)); //10 add2 = curry(add); console.log(add2(2,8)); //10
若是你想給函數執行綁定執行環境也很簡單,能夠多傳入個參數:
const curry = (fn, constext, ...arg) => { let all = arg; return (...rest) => { all.push(...rest); return fn.apply(constext, all); } }
不過到目前咱們並無實現柯里化,就是相似fn(a,b,c,d)=>fn(a)(b)(c)(d),這樣的轉化,緣由也很明顯,咱們curry以後的add2函數只能執行一次,不可以sdd2(5)(8)這樣執行,由於咱們沒有在函數第一次執行完後返回一個函數,而是返回的值,因此沒法繼續調用。
因此咱們繼續實現咱們的curry函數,要實現的點也明確了,柯里化後的函數在傳入參數未達到柯里化前的個數時候咱們不能返回值,應該返回函數讓它繼續執行(若是你閱讀到這裏能夠試着本身實現一下),下面給出一種簡單的實現方式:
const curry = (fn, ...arg) => { let all = arg || [], length = fn.length; return (...rest) => { let _args = all.slice(0); //拷貝新的all,避免改動公有的all屬性,致使屢次調用_args.length出錯 _args.push(...rest); if (_args.length < length) { return curry.call(this, fn, ..._args); } else { return fn.apply(this, _args); } } } let add2 = curry(add, 2) console.log(add2(8)); add2 = curry(add); console.log(add2(2, 8)); console.log(add2(2)(8));
這裏代碼邏輯其實很簡單,就是判斷參數是否已經達到預期的值(函數柯里化以前的參數個數),若是沒有繼續返回函數,達到了就執行函數而後返回值,惟一須要注意的點我在註釋裏寫出來了all至關於閉包引用的變量是公用的,須要在每一個返回的函數裏拷貝一份;
好了到這裏咱們基本實現了柯里化函數,咱們來看文章開始羅列的公式,細心的同窗應該能發現:
fn(a,b,c,d)=>fn(a)(b)(c)(d)();//mod1
fn(a,b,c,d)=>fn(a);fn(b);fn(c);fn(d);fn();//mod2
這兩種咱們的curry還未實現,對於這兩個公式實際上是同樣的,寫法不一樣而已,對比以前的實現就是多了一個要素,函數執行返回值的觸發時機和被柯里化函數的參數的不肯定性,好了咱們來簡單修改一下代碼:
const curry = (fn, ...arg) => { let all = arg || [], length = fn.length; return (...rest) => { let _args = all; _args.push(...rest); if (rest.length === 0) {
all=[]; return fn.apply(this, _args); } else { return curry.call(this, fn, ..._args); } } } let test = curry(function(...rest) { let args = rest.map(val => val * 10); console.log(args); }) test(2); test(2); test(3); test(); test(5); test(); test(2)(2)(2)(3)(4)(5)(6)(); test(2, 3, 4, 5, 6, 7)();
如今咱們這個test函數的參數就能夠任意傳,可多可少,至於在何時執行返回值,控制權在咱們(這裏是設置的傳入參數爲空時候觸發函數執行返回值),固然根據這邏輯咱們能改造出來不少咱們指望它按咱們需求傳參、執行的函數——這裏咱們就體會到了高階函數的靈活多變,讓使用者有更多發揮空間。
到這裏咱們科裏化基本說完了,下面咱們順帶說一下偏函數,若是你上邊柯里化的代碼都熟悉了,那麼對於偏函數的這種轉化形式應該駕輕就熟了:
fn(a,b,c,d)=>fn(a);
fn(a,b,c,d)=>fn(a,b);
咱們仍是先來看代碼吧
function part(fn, ...arg) { let all = arg || []; return (...rest) => { let args = all.slice(0); args.push(...rest); return fn.apply(this, args) } } function add(a = 0, b = 0, c = 0) { console.log(a + b + c); } let addPart = part(add); addPart(9); //9 addPart(9, 11);//20
很簡單了,咱們如今的addPar就能隨便傳參都能調用了,固然咱們也能控制函數只調用某一個或者多個參數,例如這樣:
如上圖所示,咱們想用parseInt幫咱們轉化個數組,可是咱們有沒發改動parseInt的代碼,因此控制一下傳參就好了,這樣咱們map就傳入的參數只取到第一個,獲得了咱們的指望值。
接着咱們說一說反柯里化
鑑於時間較晚了,原本想寫完的,仍是打算先休息明晚再補上吧……