這篇文章主要介紹了JavaScript中函數(shù)的Currying柯里化,Currying 的重要意義在于可以把函數(shù)完全變成"接受一個(gè)參數(shù)、返回一個(gè)值"的固定形式,需要的朋友可以參考下
引子
先來(lái)看一道小問(wèn)題:
有人在群里出了到一道題目:
var s = sum(1)(2)(3) ....... 最后 alert(s) 出來(lái)是6
var s = sum(1)(2)(3)(4) ....... 最后 alert(s) 出來(lái)是10
問(wèn)sum怎么實(shí)現(xiàn)?
剛看到題目,我第一反應(yīng)是sum返回的是一個(gè)function,但是沒(méi)有最終實(shí)現(xiàn),印象中看到過(guò)類似的原理,但是記不清了。
后來(lái)同事說(shuō),這個(gè)是叫柯里化,
實(shí)現(xiàn)方法比較巧妙:
function sum(x){
var y = function(x){
return sum(x+y)
}
y.toString = y.valueOf = function(){
return x;
}
return y;
}
下面我們就深入來(lái)看一下currying柯里化~
什么是柯里化?
柯里化是這樣的一個(gè)轉(zhuǎn)換過(guò)程,把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)(注:最初函數(shù)的第一個(gè)參數(shù))的函數(shù),如果其他的參數(shù)是必要的,返回接受余下的參數(shù)且返回結(jié)果的新函數(shù)。
當(dāng)我們這么說(shuō)的時(shí)候,我想柯里化聽(tīng)起來(lái)相當(dāng)簡(jiǎn)單。JavaScript中是怎么實(shí)現(xiàn)的呢?
假設(shè)我們要寫一個(gè)函數(shù),接受3個(gè)參數(shù)。
var sendMsg = function (from, to, msg) {
alert(["Hello " + to + ",", msg, "Sincerely,", "- " + from].join("\n"));
};
現(xiàn)在,假定我們有柯里化函數(shù),能夠把傳統(tǒng)的JavaScript函數(shù)轉(zhuǎn)換成柯里化后的函數(shù):
var sendMsgCurried = curry(sendMsg);
// returns function(a,b,c)
var sendMsgFromJohnToBob = sendMsgCurried("John")("Bob");
// returns function(c)
sendMsgFromJohnToBob("Come join the curry party!");
//=> "Hello Bob, Come join the curry party! Sincerely, - John"
手動(dòng)柯里化
在上面的例子中,我們假定擁有神秘的curry函數(shù)。我會(huì)實(shí)現(xiàn)這樣的函數(shù),但是現(xiàn)在,我們首先看看為什么這樣的函數(shù)是如此必要。
舉個(gè)例子,手動(dòng)柯里化一個(gè)函數(shù)并不困難,但是確實(shí)有點(diǎn)啰嗦:
// uncurried
var example1 = function (a, b, c) {
// do something with a, b, and c
};
// curried
var example2 = function(a) {
return function (b) {
return function (c) {
// do something with a, b, and c
};
};
};
在JavaScript,即使你不指定一個(gè)函數(shù)所有的參數(shù),函數(shù)仍將被調(diào)用。這是個(gè)非常實(shí)用JavaScript的功能,但是卻給柯里化制造了麻煩。
思路是每一個(gè)函數(shù)都是有且只有一個(gè)參數(shù)的函數(shù)。如果你想擁有多個(gè)參數(shù),你必須定義一系列相互嵌套的函數(shù)。討厭!這樣做一次兩次還可以,可是需要以這種方式定義需要很多參數(shù)的函數(shù)的時(shí)候,就會(huì)變得相當(dāng)啰嗦和難于閱讀。(但是別擔(dān)心,我會(huì)馬上告訴你一個(gè)辦法)
一些函數(shù)編程語(yǔ)言,像Haskell和OCaml,語(yǔ)法中內(nèi)置了函數(shù)柯里化。在這些語(yǔ)言中,舉個(gè)例子,每個(gè)函數(shù)是擁有一個(gè)參數(shù)的函數(shù),并且只有一個(gè)參數(shù)。你可能會(huì)認(rèn)為這種限制麻煩勝過(guò)好處,但是語(yǔ)言的語(yǔ)法就是這樣,這種限制幾乎無(wú)法察覺(jué)。
舉個(gè)例子,在OCaml,你可以用兩種方式定義上面example:
let example1 = fun a b c ->
// (* do something with a, b, c *)
let example2 = fun a ->
fun b ->
fun c ->
// (* do something with a, b, c *)
很容易看出這兩個(gè)例子和上面的那兩個(gè)例子是如何的相似。
區(qū)別,然而,是否在OCaml也是做了同樣的事情。OCaml,沒(méi)有擁有多個(gè)參數(shù)的函數(shù)。但是,在一行中聲明多個(gè)參數(shù)就是嵌套定義單參函數(shù)“快捷方式”。
類似的 ,我們期待調(diào)用柯里化函數(shù)句法上和OCaml中調(diào)用多參函數(shù)類似。我們期望這樣調(diào)用上面的函數(shù):
example1 foo bar baz
example2 foo bar baz
而在JavaScript,我們采用明顯不同的方式:
example1(foo, bar, baz);
example2(foo)(bar)(baz);
在OCaml這類語(yǔ)言中,柯里化是內(nèi)置的。在JavaScript,柯里化雖然可行(高階函數(shù)),但是語(yǔ)法上是不方便的。這也是為什么我們決定編寫一個(gè)柯里化函數(shù)來(lái)幫我們做這些繁瑣的事情,并使得我們的代碼簡(jiǎn)潔。
創(chuàng)建一個(gè)curry輔助函數(shù)
理論上我們期望可以有一個(gè)方便的方式轉(zhuǎn)換普通老式的JavaScript函數(shù)(多個(gè)參數(shù))到完全柯里化的函數(shù)。
這個(gè)想法不是我獨(dú)有的,其他的人已經(jīng)實(shí)現(xiàn)過(guò)了,例如在wu.js 庫(kù)中的.autoCurry()函數(shù)(盡管你關(guān)心的是我們自己的實(shí)現(xiàn)方式)。
首先,讓我們創(chuàng)建一個(gè)簡(jiǎn)單的輔助函數(shù) .sub_curry:
function sub_curry(fn
/*, variable number of args */
) {
var args = [].slice.call(arguments, 1);
return function () {
return fn.apply(this, args.concat(toArray(arguments)));
};
}
讓我們花點(diǎn)時(shí)間看看這個(gè)函數(shù)的功能。相當(dāng)簡(jiǎn)單。sub_curry接受一個(gè)函數(shù)fn作為它的第一個(gè)參數(shù),后面跟著任何數(shù)目的輸入?yún)?shù)。返回的是一個(gè)函數(shù),這個(gè)函數(shù)返回fn.apply執(zhí)行結(jié)果,參數(shù)序列合并了該函數(shù)最初傳入?yún)?shù)的,加上fn調(diào)用的時(shí)候傳入?yún)?shù)的。
看例子:
var fn = function(a, b, c) { return [a, b, c]; };
// these are all equivalent
fn("a", "b", "c");
sub_curry(fn, "a")("b", "c");
sub_curry(fn, "a", "b")("c");
sub_curry(fn, "a", "b", "c")();
//=> ["a", "b", "c"]
很明顯,這并不是我門想要的,但是看起來(lái)有點(diǎn)柯里化的意思了?,F(xiàn)在我們將定義柯里化函數(shù)curry:
function curry(fn, length) {
// capture fn's # of parameters
length = length || fn.length;
return function () {
if (arguments.length < length) {
// not all arguments have been specified. Curry once more.
var combined = [fn].concat(toArray(arguments));
return length - arguments.length > 0
? curry(sub_curry.apply(this, combined), length - arguments.length)
: sub_curry.call(this, combined );
} else {
// all arguments have been specified, actually call function
return fn.apply(this, arguments);
}
};
}
這個(gè)函數(shù)接受兩個(gè)參數(shù),一個(gè)函數(shù)和要“柯里化”的參數(shù)數(shù)目。第二個(gè)參數(shù)是可選的,如果省略,默認(rèn)使用Function.prototype.length 屬性,就是為了告訴你這個(gè)函數(shù)定義了幾個(gè)參數(shù)。
最終,我們能夠論證下面的行為:
var fn = curry(function(a, b, c) { return [a, b, c]; });
// these are all equivalent
fn("a", "b", "c");
fn("a", "b", "c");
fn("a", "b")("c");
fn("a")("b", "c");
fn("a")("b")("c");
//=> ["a", "b", "c"]
我知道你在想什么…
等等…什么?!
難道你瘋了?應(yīng)該是這樣!我們現(xiàn)在能夠在JavaScript中編寫柯里化函數(shù),表現(xiàn)就如同OCaml或者Haskell中的那些函數(shù)。甚至,如果我想要一次傳遞多個(gè)參數(shù),我可以向我從前做的那樣,用逗號(hào)分隔下參數(shù)就可以了。不需要參數(shù)間那些丑陋的括號(hào),即使是它是柯里化后的。
這個(gè)相當(dāng)有用,我會(huì)立即馬上談?wù)撨@個(gè),可是首先我要讓這個(gè)Curry函數(shù)前進(jìn)一小步。
柯里化和“洞”(“holes”)
盡管柯里化函數(shù)已經(jīng)很牛了,但是它也讓你必須花費(fèi)點(diǎn)小心思在你所定義函數(shù)的參數(shù)順序上。終究,柯里化的背后思路就是創(chuàng)建函數(shù),更具體的功能,分離其他更多的通用功能,通過(guò)分步應(yīng)用它們。
當(dāng)然這個(gè)只能工作在當(dāng)最左參數(shù)就是你想要分步應(yīng)用的參數(shù)!
為了解決這個(gè),在一些函數(shù)式編程語(yǔ)言中,會(huì)定義一個(gè)特殊的“占位變量”。通常會(huì)指定下劃線來(lái)干這事,如過(guò)作為一個(gè)函數(shù)的參數(shù)被傳入,就表明這個(gè)是可以“跳過(guò)的”。是尚待指定的。
這是非常有用的,當(dāng)你想要分步應(yīng)用(partially apply)一個(gè)特定函數(shù),但是你想要分布應(yīng)用(partially apply)的參數(shù)并不是最左參數(shù)。
舉個(gè)例子,我們有這樣的一個(gè)函數(shù):
var sendAjax = function (url, data, options) {
/* ... */
}
也許我們想要定義一個(gè)新的函數(shù),我們部分提供SendAjax函數(shù)特定的Options,但是允許url和data可以被指定。
當(dāng)然了,我們能夠相當(dāng)簡(jiǎn)單的這樣定義函數(shù):
var sendPost = function (url, data) {
return sendAjax(url, data, { type: "POST", contentType: "application/json" });
};
或者,使用使用約定的下劃線方式,就像下面這樣:
var sendPost = sendAjax( _ , _ , { type: "POST", contentType: "application/json" });
注意兩個(gè)參數(shù)以下劃線的方式傳入。顯然,JavaScript并不具備這樣的原生支持,于是我們?cè)鯓硬拍苓@樣做呢?
回過(guò)頭讓我們把curry函數(shù)變得智能一點(diǎn)…
首先我們把我們的“占位符”定義成一個(gè)全局變量。
var _ = {};
我們把它定義成對(duì)象字面量{},便于我們可以通過(guò)===操作符來(lái)判等。
不管你喜不喜歡,為了簡(jiǎn)單一點(diǎn)我們就使用_來(lái)做“占位符”?,F(xiàn)在我們就可以定義新的curry函數(shù),就像下面這樣:
function curry (fn, length, args, holes) {
length = length || fn.length;
args = args || [];
holes = holes || [];
return function(){
var _args = args.slice(0),
_holes = holes.slice(0),
argStart = _args.length,
holeStart = _holes.length,
arg, i;
for(i = 0; i < arguments.length; i++) {
arg = arguments[i];
if(arg === _ && holeStart) {
holeStart--;
_holes.push(_holes.shift());
// move hole from beginning to end
} else if (arg === _) {
_holes.push(argStart + i);
// the position of the hole.
} else if (holeStart) {
holeStart--;
_args.splice(_holes.shift(), 0, arg);
// insert arg at index of hole
} else {
_args.push(arg);
}
}
if(_args.length < length) {
return curry.call(this, fn, length, _args, _holes);
} else {
return fn.apply(this, _args);
}
}
}
實(shí)際代碼還是有著巨大不同的。 我們這里做了一些關(guān)于這些“洞”(holes)參數(shù)是什么的記錄。概括而言,運(yùn)行的職責(zé)是相同的。
展示下我們的新幫手,下面的語(yǔ)句都是等價(jià)的:
var f = curry(function(a, b, c) { return [a, b, c]; });
var g = curry(function(a, b, c, d, e) { return [a, b, c, d, e]; });
// all of these are equivalent
f("a","b","c");
f("a")("b")("c");
f("a", "b", "c");
f("a", _, "c")("b");
f( _, "b")("a", "c");
//=> ["a", "b", "c"]
// all of these are equivalent
g(1, 2, 3, 4, 5);
g(_, 2, 3, 4, 5)(1);
g(1, _, 3)(_, 4)(2)(5);
//=> [1, 2, 3, 4, 5]
瘋狂吧?!
我為什么要關(guān)心?柯里化能夠怎么幫助我?
你可能會(huì)停在這兒思考…
這看起來(lái)挺酷而且…但是這真的能幫助我編寫更好的代碼?
這里有很多原因關(guān)于為什么函數(shù)柯里化是有用的。
函數(shù)柯里化允許和鼓勵(lì)你分隔復(fù)雜功能變成更小更容易分析的部分。這些小的邏輯單元顯然是更容易理解和測(cè)試的,然后你的應(yīng)用就會(huì)變成干凈而整潔的組合,由一些小單元組成的組合。
為了給一個(gè)簡(jiǎn)單的例子,讓我們分別使用Vanilla.js, Underscore.js, and “函數(shù)化方式” (極端利用函數(shù)化特性)來(lái)編寫CSV解析器。
Vanilla.js (Imperative)
//+ String -> [String]
var processLine = function (line){
var row, columns, j;
columns = line.split(",");
row = [];
for(j = 0; j < columns.length; j++) {
row.push(columns[j].trim());
}
};
//+ String -> [[String]]
var parseCSV = function (csv){
var table, lines, i;
lines = csv.split("\n");
table = [];
for(i = 0; i < lines.length; i++) {
table.push(processLine(lines[i]));
}
return table;
};
Underscore.js
//+ String -> [String]
var processLine = function (row) {
return _.map(row.split(","), function (c) {
return c.trim();
});
};
//+ String -> [[String]]
var parseCSV = function (csv){
return _.map(csv.split("\n"), processLine);
};
函數(shù)化方式
//+ String -> [String]
var processLine = compose( map(trim) , split(",") );
//+ String -> [[String]]
var parseCSV = compose( map(processLine) , split("\n") );
所有這些例子功能上是等價(jià)的。我有意的盡可能的簡(jiǎn)單的編寫這些。
想要達(dá)到某種效果是很難的,但是主觀上這些例子,我真的認(rèn)為最后一個(gè)例子,函數(shù)式方式的,體現(xiàn)了函數(shù)式編程背后的威力。
關(guān)于curry性能的備注
一些極度關(guān)注性能的人可以看看這里,我的意思是,關(guān)注下所有這些額外的事情?
通常,是這樣,使用柯里化會(huì)有一些開(kāi)銷。取決于你正在做的是什么,可能會(huì)或不會(huì),以明顯的方式影響你。也就是說(shuō),我敢說(shuō)幾乎大多數(shù)情況,你的代碼的擁有性能瓶頸首先來(lái)自其他原因,而不是這個(gè)。
有關(guān)性能,這里有一些事情必須牢記于心:
1.存取arguments對(duì)象通常要比存取命名參數(shù)要慢一點(diǎn)
2.一些老版本的瀏覽器在arguments.length的實(shí)現(xiàn)上是相當(dāng)慢的
3.使用fn.apply( … ) 和 fn.call( … )通常比直接調(diào)用fn( … ) 稍微慢點(diǎn)
4.創(chuàng)建大量嵌套作用域和閉包函數(shù)會(huì)帶來(lái)花銷,無(wú)論是在內(nèi)存還是速度上
5.在大多是web應(yīng)用中,“瓶頸”會(huì)發(fā)生在操控DOM上。這是非常不可能的,你在所有方面關(guān)注性能。顯然,用不用上面的代碼自行考慮。