導(dǎo)讀:函數(shù)柯里化currying的概念最早由俄國數(shù)學(xué)家Moses Sch?nfinkel發(fā)明肚医,而后由著名的數(shù)理邏輯學(xué)家Haskell Curry將其豐富和發(fā)展觉既,currying由此得名抹剩。
定義:currying又稱部分求值。一個(gè)currying的函數(shù)首先會(huì)接受一些參數(shù),接受了這些參數(shù)之后驯嘱,該函數(shù)并不會(huì)立即求值假瞬,而是繼續(xù)返回另外一個(gè)函數(shù)陕靠,剛才傳入的參數(shù)在函數(shù)形成的閉包中被保存起來。待到函數(shù)被真正需要求值的時(shí)候脱茉,之前傳入的所有參數(shù)都會(huì)被一次性用于求值剪芥。
一個(gè)簡單curry的栗子
function add(a, b) {
return a + b;
}
//函數(shù)只能傳一個(gè)參數(shù)時(shí)候?qū)崿F(xiàn)加法
function curry(a) {
return function(b) {
return a + b;
}
}
var add2 = curry(2); //add2也就是第一個(gè)參數(shù)為2的add版本
console.log(add2(3))//5
通過以上簡單介紹我們大概了解了,函數(shù)柯里化基本是在做這么一件事情:只傳遞給函數(shù)一部分參數(shù)來調(diào)用它琴许,讓它返回一個(gè)函數(shù)去處理剩下的參數(shù)税肪。用公式表示就是我們要做的事情其實(shí)是
fn(a,b,c,d)=>fn(a)(b)(c)(d);
fn(a,b,c,d)=>fn(a,b)(c)(d)益兄;
fn(a,b,c,d)=>fn(a)(b锻梳,c,d)净捅;
......
再或者這樣:
fn(a,b,c,d)=>fn(a)(b)(c)(d)()疑枯;
fn(a,b,c,d)=>fn(a);fn(b)蛔六;fn(c)荆永;fn(d);fn()国章;
但不是這樣:
fn(a,b,c,d)=>fn(a)具钥;
fn(a,b,c,d)=>fn(a,b)捉腥;
......
這類不屬于柯里化內(nèi)容氓拼,它也有個(gè)專業(yè)的名字叫偏函數(shù),這個(gè)之后我們也會(huì)提到抵碟。
下面我們繼續(xù)把之前的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
如果你想給函數(shù)執(zhí)行綁定執(zhí)行環(huán)境也很簡單桃漾,可以多傳入個(gè)參數(shù):
const curry = (fn, constext, ...arg) => {
let all = arg;
return (...rest) => {
all.push(...rest);
return fn.apply(constext, all);
}
}
不過到目前我們并沒有實(shí)現(xiàn)柯里化,就是類似fn(a,b,c,d)=>fn(a)(b)(c)(d)拟逮,這樣的轉(zhuǎn)化撬统,原因也很明顯,我們curry之后的add2函數(shù)只能執(zhí)行一次敦迄,不能夠sdd2(5)(8)這樣執(zhí)行恋追,因?yàn)槲覀儧]有在函數(shù)第一次執(zhí)行完后返回一個(gè)函數(shù),而是返回的值罚屋,所以無法繼續(xù)調(diào)用苦囱。
所以我們繼續(xù)實(shí)現(xiàn)我們的curry函數(shù),要實(shí)現(xiàn)的點(diǎn)也明確了脾猛,柯里化后的函數(shù)在傳入?yún)?shù)未達(dá)到柯里化前的個(gè)數(shù)時(shí)候我們不能返回值撕彤,應(yīng)該返回函數(shù)讓它繼續(xù)執(zhí)行(如果你閱讀到這里可以試著自己實(shí)現(xiàn)一下),下面給出一種簡單的實(shí)現(xiàn)方式:
const curry = (fn, ...arg) => {
let all = arg || [],
length = fn.length;
return (...rest) => {
let _args = all.slice(0); //拷貝新的all猛拴,避免改動(dòng)公有的all屬性羹铅,導(dǎo)致多次調(diào)用_args.length出錯(cuò)
_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));//10
console.log(add2(8, 1));//10
console.log(add2(8)(1));//error
add2 = curry(add);
console.log(add2(2, 8));
console.log(add2(2)(8));
let test = curry(function(a, b, c) {
console.log(a + b + c);
})
test(1, 2, 3);
test(1, 2)(3);
test(1)(2)(3);
這里代碼邏輯其實(shí)很簡單,就是判斷參數(shù)是否已經(jīng)達(dá)到預(yù)期的值(函數(shù)柯里化之前的參數(shù)個(gè)數(shù))愉昆,如果沒有繼續(xù)返回函數(shù)职员,達(dá)到了就執(zhí)行函數(shù)然后返回值,唯一需要注意的點(diǎn)我在注釋里寫出來了all相當(dāng)于閉包引用的變量是公用的跛溉,需要在每個(gè)返回的函數(shù)里拷貝一份焊切;
好了到這里我們基本實(shí)現(xiàn)了柯里化函數(shù)扮授,我們來看文章開始羅列的公式,細(xì)心的同學(xué)應(yīng)該能發(fā)現(xiàn):
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還未實(shí)現(xiàn)诺苹,對于這兩個(gè)公式其實(shí)是一樣的,寫法不同而已雹拄,對比之前的實(shí)現(xiàn)就是多了一個(gè)要素收奔,函數(shù)執(zhí)行返回值的觸發(fā)時(shí)機(jī)和被柯里化函數(shù)的參數(shù)的不確定性,好了我們來簡單修改一下代碼:
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();//[20, 20, 30]
test(5);
test();//[50]
test(2)(2)(2)(3)(4)(5)(6)();//
test(2, 3, 4, 5, 6, 7)();//
現(xiàn)在我們這個(gè)test函數(shù)的參數(shù)就可以任意傳滓玖,可多可少坪哄,至于在什么時(shí)候執(zhí)行返回值,控制權(quán)在我們(這里是設(shè)置的傳入?yún)?shù)為空時(shí)候觸發(fā)函數(shù)執(zhí)行返回值)势篡,當(dāng)然根據(jù)這邏輯我們能改造出來很多我們期望它按我們需求傳參翩肌、執(zhí)行的函數(shù)——這里我們就體會(huì)到了高階函數(shù)的靈活多變,讓使用者有更多發(fā)揮空間禁悠。
到這里我們科里化基本說完了念祭,下面我們順帶說一下偏函數(shù),如果你上邊柯里化的代碼都熟悉了碍侦,那么對于偏函數(shù)的這種轉(zhuǎn)化形式應(yīng)該得心應(yīng)手了:
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
很簡單了站玄,我們現(xiàn)在的addPar就能隨便傳參都能調(diào)用了濒旦,當(dāng)然我們也能控制函數(shù)之調(diào)用某一個(gè)或者多個(gè)參數(shù)株旷,例如這樣:
//偏han shu
function part(fn) {
return (...arguments) => {
return fn.call(this, arguments[0])
}
};
let newA = ['33','222','999','99888','2345'].map(part(parseInt));
console.log('newA is ', newA)
我們想用parseInt幫我們轉(zhuǎn)化個(gè)數(shù)組疤估,但是我們沒法改動(dòng)parseInt的代碼,所以控制一下傳參就行了铃拇,這樣我們map就傳入的參數(shù)只取到第一個(gè)钞瀑,得到了我們的期望值。
Function.prototype.bind 方法也是柯里化應(yīng)用
與 call/apply 方法直接執(zhí)行不同雕什,bind 方法 將第一個(gè)參數(shù)設(shè)置為函數(shù)執(zhí)行的上下文,其他參數(shù)依次傳遞給調(diào)用方法(函數(shù)的主體本身不執(zhí)行贷岸,可以看成是延遲執(zhí)行)壹士,并動(dòng)態(tài)創(chuàng)建返回一個(gè)新的函數(shù), 這符合柯里化特點(diǎn)偿警。
var foo = {x: 888};
var bar = function () {
console.log(this.x);
}.bind(foo); // 綁定
bar(); // 888
下面是一個(gè) bind 函數(shù)的模擬躏救,testBind 創(chuàng)建并返回新的函數(shù)螟蒸,在新的函數(shù)中將真正要執(zhí)行業(yè)務(wù)的函數(shù)綁定到實(shí)參傳入的上下文,延遲執(zhí)行了七嫌。
Function.prototype.testBind = function (scope) {//提前固定易變參數(shù)。
var fn = this; // this 指向的是調(diào)用 testBind 方法的一個(gè)函數(shù)英妓,
return function () {
return fn.apply(scope);
}
};
var testBindBar = bar.testBind(foo); // 綁定 foo绍赛,延遲執(zhí)行
console.log(testBindBar); // Function (可見蔓纠,bind之后返回的是一個(gè)延遲執(zhí)行的新函數(shù))
testBindBar();
這里要注意 prototype 中 this 的理解惹资。
反柯里化
Array.prototype上的方法原本只能用來操作array對象。但用call和apply可以把任意對象當(dāng)作this傳入某個(gè)方法猴誊,這樣一來侮措,方法中用到this的地方就不再局限于原來規(guī)定的對象,而是加以泛化并得到更廣的適用性
有沒有辦法把泛化this的過程提取出來呢分扎?反柯里化(uncurrying)就是用來解決這個(gè)問題的。反柯里化主要用于擴(kuò)大適用范圍墨状,創(chuàng)建一個(gè)應(yīng)用范圍更廣的函數(shù)菲饼。使本來只有特定對象才適用的方法,擴(kuò)展到更多的對象宏悦。
uncurrying的話題來自JavaScript之父Brendan Eich在2011年發(fā)表的一篇文章包吝。以下代碼是 uncurrying 的實(shí)現(xiàn)方式之一:
Function.prototype.uncurrying = function () {
var _this = this;
return function() {
var obj = Array.prototype.shift.call( arguments );
return _this.apply( obj, arguments );
};
};
另一種實(shí)現(xiàn)方法如下
Function.prototype.currying = function() {
var _this = this;
return function() {
return Function.prototype.call.apply(_this, arguments);
}
}
最終是都把this.method轉(zhuǎn)化成method(this,arg1,arg2....)以實(shí)現(xiàn)方法借用和this的泛化
下面是一個(gè)讓普通對象具備push方法的例子
var push = Array.prototype.push.uncurrying(),
obj = {};
push(obj, 'first', 'two');
console.log(obj);
/*obj {
0 : "first",
1 : "two"
}*/
通過uncurrying的方式源葫,Array.prototype.push.call變成了一個(gè)通用的push函數(shù)。這樣一來嚷狞,push函數(shù)的作用就跟Array.prototype.push一樣了储矩,同樣不僅僅局限于只能操作array對象。而對于使用者而言持隧,調(diào)用push函數(shù)的方式也顯得更加簡潔和意圖明了
最后逃片,再看一個(gè)例子
var toUpperCase = String.prototype.toUpperCase.uncurrying();
console.log(toUpperCase('avd')); // AVD
function AryUpper(ary) {
return ary.map(toUpperCase);
}
console.log(AryUpper(['a', 'b', 'c'])); // ["A", "B", "C"]