為你的 Javascript 加點(diǎn)咖喱
咖喱,是一種由多種香料調(diào)配而成的調(diào)料,常見于印度菜、泰國菜和日本菜等 ( by the way, 咖喱雞塊我的愛 ),一般伴隨肉類和飯一起吃。關(guān)于咖喱的介紹詳見維基百科: http://zh.wikipedia.org/wiki/%E5%92%96%E5%93%A9 。
當(dāng)然,這篇文章不是一篇食譜,以在下的廚藝,目前只能停留在吃咖喱的階段。咖喱的英文寫作 Curry ,這個詞還有另一層含義,為一種基于 Haskell 的實(shí)驗式的函數(shù)編程語言,混合了 函數(shù) 與 邏輯編程 ,也加入了 約束編程 的特性。取名于數(shù)學(xué)家 Haskell Curry 。不得不說這位數(shù)學(xué)家對計算機(jī)的影響還真是很大。
However, 這里要談的不是這位偉大的數(shù)學(xué)家,也不是 Haskell 語言,下面我先描述一個場景。
Javascript 作為一種函數(shù)式的編程語言,函數(shù)的使用稱之為應(yīng)用 ( applied ),有一定 JS 編程經(jīng)驗的開發(fā)者很快能想到內(nèi)置的 Function.prototype.apply() 方法,在應(yīng)用函數(shù)的時候,我們可以這樣寫:
1 var sayHello = function(param) { 2 return 'Hello, ' + param; 3 } 4 5 sayHello.apply(null, ['Tom']); // 'Hello, Tom'
sayHello() 內(nèi)部的 this 指向全局對象。或者也可以讓 JS 為我們完成數(shù)學(xué)運(yùn)算:
1 function add(x, y) { 2 return x + y; 3 } 4 5 add.apply(null, [1, 2]); // 3
這里應(yīng)用函數(shù)的時候,我們一次性將所有的參數(shù)都傳了進(jìn)去,然而在實(shí)際應(yīng)用中,我們可能會遇到不需要一次傳入所有參數(shù)的情況,若 add() 函數(shù)的寫法不變的話,只傳入一個參數(shù)的時候就會產(chǎn)生錯誤,這不是我們希望看到的。
我們希望看到什么呢,在這里我們先 YY 一下:
1 var add = function(x, y) { 2 return x + y; 3 } 4 5 // 部分應(yīng)用 6 var partialAdd = add.partialApply(null, [1]); 7 partialAdd.apply(null, [2]); // 3
部分應(yīng)用的步驟為我們提供了一個可供調(diào)用的新函數(shù),隨后我們可以使用其他參數(shù)來調(diào)用這個新函數(shù)。看起來很美嗎?可惜的是,Javascript 中并沒有 partialApply() 這樣的方法和函數(shù)。不過前端開發(fā)者都是有進(jìn)取精神的小強(qiáng),再加上 JS 是一門靈活性很強(qiáng)的語言,我們完全可以構(gòu)造出這樣的部分應(yīng)用函數(shù),來滿足我們的需求。
扯了這些,和之前提到的咖喱有關(guān)系嗎?有的,這種是函數(shù)理解并處理部分應(yīng)用場景的過程我們稱之為 Curry 過程(也叫做函數(shù)的柯里化)。(別噴我,寫一篇能讓人有興趣看下去的文章好難,不寫點(diǎn)吸引人眼球的東西沒人看啊)
回到之前所說,對于一個簡短的加法運(yùn)算,比如 1 + 2,部分應(yīng)用的實(shí)現(xiàn)思路可以寫成 add(1)(2),這就要求我們在調(diào)用 add(1) 后不僅不會產(chǎn)生錯誤,更要返回一個能夠繼續(xù)傳入第二個參數(shù)的函數(shù),說到這里答案已經(jīng)呼之欲出了:
1 function add(x, y) { 2 // 部分 3 if (typeof y === 'undefined') { 4 return function(y) { 5 return x + y; 6 } 7 } 8 9 // 完全 10 return x + y; 11 }
其實(shí)這是一種常見的函數(shù)變換技巧,稱為函數(shù)的不完全調(diào)用 (partial application)。這種函數(shù)變換的特點(diǎn)是每次調(diào)用都返回一個參數(shù),直到得到最終運(yùn)行結(jié)果為止。不過我們能用一種更為通用的方法來處理相同的任務(wù)嗎,答案自然是肯定的:
function curry(fn) { var slice = Array.prototype.slice, stored_args = slice.call(arguments, 1); return function() { var new_args = slice.call(arguments), args = stored_args.concat(new_args); return fn.apply(null, args); } }
這樣,在返回的函數(shù)中便保存了已經(jīng)傳入的參數(shù),保存在變量 a 中,在返回的函數(shù)開頭,剝離了第一個參數(shù),因為這個參數(shù)是即將被 curry 化的函數(shù),同時也保存了指向 slice() 方法的私有引用。當(dāng)我們訪問返回的函數(shù)時,新函數(shù)將原有的部分應(yīng)用參數(shù)合并到新參數(shù),再將合并后的參數(shù)應(yīng)用到原始的函數(shù)中。
這樣,使任意函數(shù) curry 化的通用方法就有了,可將之前定義的 add() 函數(shù)用來測試。
// 普通函數(shù) function add(x, y) { return x + y; } // 將一個函數(shù) curry 化以獲得一個新的函數(shù) var newadd = curry(add, 1); newadd(2); // 另一種方法 curry(add, 3)(4); // 連續(xù) curry 化 var newadd = curry(add, 1); var anothernewadd = curry(newadd, 2);
實(shí)際上,這些只是拋磚引玉,curry 化可改進(jìn)的地方還有很多,比如當(dāng)對參數(shù)的類型和順序有要求時如何根據(jù)實(shí)際情況編寫適合的 curry 化函數(shù)等。后續(xù)我會對內(nèi)容作更多的補(bǔ)充,也歡迎各位暢所欲言,多提意見。

浙公網(wǎng)安備 33010602011771號