什么是 curry 化
curry 化也是一個(gè)常見的概念,維基百科對(duì)其解釋為:
在計(jì)算機(jī)科學(xué)中,柯里化(currying),又譯為卡瑞化或加里化,是把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)(最初函數(shù)的第一個(gè)參數(shù))的函數(shù),并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)。這個(gè)技術(shù)由克里斯托弗·斯特雷奇以邏輯學(xué)家哈斯凱爾·加里命名的。
再簡(jiǎn)潔一些就是:柯里化是把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)的函數(shù),并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)。
還是不懂,沒關(guān)系,下面就通過幾個(gè)例子一步一步去了解柯里化。
怎么實(shí)現(xiàn) curry 化
實(shí)現(xiàn)一個(gè)函數(shù),對(duì)數(shù)組進(jìn)行過濾,過濾掉小于10的項(xiàng)。
傳統(tǒng)的做法是:
const filterLowerThan10 = (array) => {
let result = [];
for (let i = 0; i < array.length; i++) {
let currentValue = array[i];
if (currentValue < 10) {
result.push(currentValue);
}
}
return result;
};
實(shí)現(xiàn)起來(lái)并沒有難度,但是當(dāng)前要過濾的是小于10的項(xiàng),如果這個(gè)閾值更改了呢,我們可以借用 curry 化的思想將其改造:
const filterLowerNumber = (number) => {
return (array) => {
let result = [];
for (let i = 0; i < array.length; i++) {
let currentValue = array[i];
if (currentValue < number) {
result.push(currentValue);
}
}
return result;
};
};
const filterLowerThan10 = filterLowerNumber(10);
filterLowerThan10([1, 11, 8, 21, 2]); // [1,8,2]
// 也可以這樣簡(jiǎn)寫
// filterLowerNumber(10)([1, 11, 8, 21, 2]);
另一個(gè)場(chǎng)景
實(shí)現(xiàn)一個(gè)求兩數(shù)之和得方法
普通函數(shù):
function add(x, y) {
return x + y;
}
add(1, 2); // 3
curry 化函數(shù):
var add = function (x) {
return (y) => x + y;
};
add(1)(2); // 3
在此基礎(chǔ)上提交更復(fù)雜得要求
按要求實(shí)現(xiàn) add 方法:
add(1)(2); // 結(jié)果為3
add(1)(3)(5); // 結(jié)果為9
add(1)...(n); // 結(jié)果為sum
大家可能比較眼熟哈,很多 curry 化的面試題都是以此為原型的。
解題:
- 由調(diào)用方式可知,add 函數(shù)每次執(zhí)行后一定返回一個(gè)函數(shù),以供后續(xù)調(diào)用,且返回的函數(shù)依然要返回自身,供多級(jí)調(diào)用;
- 當(dāng)最后一次調(diào)用結(jié)束,返回的是一個(gè)函數(shù),為了滿足題意,需要改寫內(nèi)部返回的函數(shù) toString (代碼中也解釋);
- 為了進(jìn)行求和,需要在 add 函數(shù)內(nèi)部維護(hù)一個(gè)閉包變量 args,args 是個(gè)數(shù)組,存放了第一次調(diào)用 add 和 后續(xù)調(diào)用 fn 函數(shù)時(shí)傳入的參數(shù);
- 在調(diào)用 fn 的 toString 方法時(shí),意味著最后一次調(diào)用結(jié)束,返回函數(shù),那么就計(jì)算 args 數(shù)組中的所有值得和即可求出結(jié)果。
const add = (arg1) => {
let args = [arg1];
const fn = (arg2) => {
args.push(arg2);
return fn;
};
// 因?yàn)樽詈笠淮螆?zhí)行完畢后會(huì)返回 fn 函數(shù)體,相當(dāng)于調(diào)用了 fn 的 toString 方法,所以改寫 toString 方法求和即可
fn.toString = function () {
return args.reduce((prev, item) => prev + item, 0);
};
return fn;
};
add(1)(2)(3); // 6
這里只實(shí)現(xiàn)了每次調(diào)用傳入單個(gè)參數(shù),為了支持每次調(diào)用可以傳入多參數(shù),改動(dòng)為:
const add = (...arg1) => {
let args = [...arg1];
const fn = (...arg2) => {
args = [...args, ...arg2];
return fn;
};
fn.toString = function () {
return args.reduce((prev, item) => prev + item, 0);
};
return fn;
};
add(1)(2, 3, 4)(5); // 17
雖然可以正確計(jì)算出結(jié)果,但是如果用 === 把表達(dá)式和結(jié)果進(jìn)行一個(gè)判斷
add(1)(2)(3) === 6; //false
add(1)(2, 3, 4)(5) === 15; //false
無(wú)一例外輸出都是false,其實(shí)并不奇怪,上面代碼中也說(shuō)過,調(diào)用 add 函數(shù)返回的永遠(yuǎn)都是 Function ,這里只是通過修改了 fn 的 toString 方法達(dá)到了輸出計(jì)算結(jié)果的目的,但是這并不能改變返回值的類型,依然是 Function。
反 curry 化
反 curry 化的意義在于擴(kuò)大函數(shù)的適用性,使本來(lái)作為特定對(duì)象所擁有的功能函數(shù)可以被任意對(duì)象所使用。
function Person() {
this.message = "wowowo";
}
Person.prototype = {
speak: function () {
console.log(this.message);
},
};
Person 實(shí)例均可使用 speak 方法:
new Person().speak();
如果有一個(gè)變量對(duì)象:
const dog = {
message: "wang wang wang!",
};
該對(duì)象也想使用 Person 原型上的 speak 方法,就需要反 curry 化:
const unCurrySpeak = unCurry(Person.prototype.speak);
unCurrySpeak(dog);
unCurry 就是我們要實(shí)現(xiàn)的反 curry 化的方法。
分析可知: unCurry 的參數(shù)是一個(gè)“希望被其他對(duì)象所調(diào)用的方法”,unCurry 執(zhí)行后返回一個(gè)新的函數(shù),該函數(shù)的第一個(gè)參數(shù)是預(yù)期要執(zhí)行方法的對(duì)象(dog),后面的參數(shù)是執(zhí)行這個(gè)方法時(shí)需要傳遞的參數(shù)。
function unCurry(fn) {
return function () {
var obj = [].shift.call(arguments);
return fn.apply(obj, arguments);
};
}
如此實(shí)現(xiàn)即可,當(dāng)然也可以將 uncurry 掛載在函數(shù)原型上實(shí)現(xiàn)。