下面來一起看看究竟什麼是函數柯里化
維基百科的解釋是:把接收多個參數的函數變換成接收一個單一參數(最初函數的第一個參數)的函數,並返回接受剩餘的參數而且返回結果的新函數的技術。其由數學家Haskell Brooks Curry提出,並以curry命名。
概念往往都是乾澀且難懂的,讓我們用人話來解釋就是:如果我們不確定這個函數有多少個參數,我們可以先給它傳入一個參數,然後通過JS閉包來進行返回一個函數,內部函數接收除開第一個參數外的其餘參數進行操作並輸出,這個就是函數的柯里化;
舉個小例子:
場景(需求):
衆所周知程序員每天加班的時間還是比較多的,如果我們需要計算一個程序員每天的加班時間,那麼我們的第一反應應該是這樣;
var overtime=0;
function time(x){
return overtime+=x;
}
time(1); //1
time(2); //3
time(3); //6
上面的代碼固然沒有問題,可是需要每天調用都算加一下當天的時間,很麻煩,並且每調用一次函數都要進行一定的操作,如果數據量巨大,有可能會有影響性能的風險,那麼有沒有可以偷懶又能解決問題的辦法呢?有的!
function time(x){
return function(y){
return x+y;
}
}
var times=time(0);
times(3);
但是上面代碼依然存在問題,在實際開發中很多時候我們的參數是不確定的,上面代碼雖然簡單的實現了柯里化的基本操作,但是對於參數不確定的情況是處理不了的;所以存在着函數參數的侷限性;不過我們從上面的代碼中基本可以知道函數柯里化是個啥意思了;就是一個函數調用的時候只允許傳入一個參數,然後通過閉包返回內部函數去處理和接收剩餘參數,返回的函數通過閉包的方式記住了time的第一個參數;
我們再來把代碼改造一下:
// 首先定義一個變量接收函數
var overtime = (function() {
//定義一個數組用來接收參數
var args = [];
//這裏運用閉包,調用外部函數返回一個內部函數
return function() {
//arguments是瀏覽器內置對象,專門用來接收參數
//如果參數的長度爲0即沒有參數的時候
if(arguments.length === 0) {
//定義變量用來累加
var time = 0;
//循環累加,用i和args的長度進行比較
for (var i = 0, l = args.length; i < l; i++) {
//進行累加操作 等價於time=time+args[i]
time += args[i];
}
// 返回累加的結果
return time;
//如果arguments對象參數長度不爲零,即有參數的時候
}else {
//定義的空數組添加arguments參數作爲數組項,第一個參數古args作爲改變this指向,第二個參數arguments把剩餘參數作爲數組形式添加至空數組中
[].push.apply(args, arguments);
}
}
})();
overtime(3.5); // 第一天
overtime(4.5); // 第二天
overtime(2.1); // 第三天
//...
console.log( overtime() ); // 10.1
代碼經過我們的改造已經實現了功能,但是這不是一個函數柯里化的完整實現,那麼我們要怎麼完整實現呢?下面我們來介紹一種通用的實現方式:
通用的實現方式:
//定義方法currying,先傳入一個參數
var currying=function(fn){
//定義空數組裝arguments對象的剩餘參數
var args=[];
//利用閉包返回一個函數處理剩餘參數
return function (){
//如果arguments的參數長度爲0,即沒有剩餘參數
if(arguments.length===0){
//執行上面方法
return fn.apply(this,args)
}
console.log(arguments)
//如果arguments的參數長度不爲0,即還有剩餘參數
//在數組的原型對象上添加數組,apply用來更改this的指向爲args
//將[].slice.call(arguments)的數組添加到原型數組上
Array.prototype.push.apply(args,[].slice.call(arguments))
//args.push([].slice.call(arguments))
console.log(args)
//這裏返回的arguments.callee是返回的閉包函數,callee是arguments對象裏面的一個屬性,用於返回正被執行的function對象
return arguments.callee
}
}
//這裏調用currying方法並傳入add函數,結果會返回閉包內部函數
var s=currying(add);
//調用閉包內部函數,當有參數的時候會將參數逐步添加到args數組中,待沒有參數傳入的時候直接調用
//調用的時候支持鏈式操作
s(1)(2)(3)();
//也可以一次性傳入多個參數
s(1,2,3);
console.log(s());
JS函數柯里化的優點:
1.可以延遲計算,即如果調用柯里化函數傳入參數是不調用的,會將參數添加到數組中存儲,等到沒有參數傳入的時候進行調用;
2.參數複用,當在多次調用同一個函數,並且傳遞的參數絕大多數是相同的,那麼該函數可能是一個很好的柯里化候選。
世間萬物相對,有因必有果,當然了,有柯里化必然有反柯里化;
反柯里化(uncurrying)
從字面意思上來講就是跟柯里化的意思相反;其實真正的反柯里化的作用是擴大適用範圍,就是說當我們調用某個方法的時候,不需要考慮這個對象自身在設計的過程中有沒有這個方法,只要這個方法適用於它,我們就可以使用;(這裏引用的是動態語言中的鴨子類型的思想)
在學習JS反柯里化之前,我們先學習一下動態語言的鴨子類型思想,以助於我們更好的理解:
動態語言鴨子類型思想(維基百科解釋):
在程序設計中,鴨子類型(duck typing)是動態類型的一種風格。
在這種風格中,一個對象有效的語義,不是由繼承自特定的類或實現特定的接口,而是由當前方法和屬性的集合決定。
這個概念的名字來源於由 James Whitcomb Riley 提出的鴨子測試,“鴨子測試”可以這樣表述:
當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱爲鴨子。
理論上的解釋往往乾澀難懂,換成人話來說就是:你是你媽媽的兒子/女兒,不管你是否優秀,是否漂亮,只要你是你媽親生的,那麼你就是你媽的兒子/女兒;換成鴨子類型就是,只要你會呱呱叫,走起來像鴨子,只要你擁有的行爲像鴨子,不管你是不是鴨子,那麼你就可以被稱爲鴨子;
在Javascript中有很多鴨子類型的引用,比如我們在對一個變量進行賦值的時候,顯然是不需要考慮對象的類型的,正是因爲如此,Javascript才更加的靈活,所以Javascript是一門典型的動態類型語言;
我們來看一下反柯里化中是怎麼引用鴨子類型的:
//函數原型對象上添加uncurring方法
Function.prototype.uncurring = function() {
//改變this的指向
//這裏的this指向是Array.prototype.push
var self = this;
//這裏的閉包用來返回內部函數的執行
return function() {
//創建一個變量,在數組的原型對象上添加shift上面刪除第一個參數
//改變數組this的指向爲arguments
var obj = Array.prototype.shift.call(arguments);
//最後返回執行並給方法改變指向爲obj也就是arguments
// 並傳入arguments作爲參數
return self.apply(obj, arguments);
};
};
//數組原型對象上添加uncurrying方法
var push = Array.prototype.push.uncurring();
//測試一下
//匿名函數自執行
(function() {
//這裏的push就是一個函數方法了
//相當於傳入參數arguments和4兩個參數,但是在上面shift方法中刪除第一個參數,這裏的arguments參數被截取了,所以最後實際上只傳入了4
push(arguments, 4);
console.log(arguments); //[1, 2, 3, 4]
//匿名函數自調用並帶入參數1,2,3
})(1, 2, 3)
到這裏大家可以想一想arguments是一個接收參數的對象,裏面是沒有push方法的,那麼arguments爲什麼能調用push方法呢?
這是因爲代碼var push = Array.prototype.push.uncurring();在數組的原型對象的push方法上添加了uncurring方法,然後在執行匿名函數的方法push(arguments, 4);時候實質上是在調用上面的方法在Function的原型對象上添加uncurring方法並返回一個閉包內部函數執行,在執行的過程中因爲Array原型對象上的shift方法會把 push(arguments, 4);中的arguments截取,所以其實方法的實際調用是push(4),所以最終的結果纔是[1,2,3,4]