怎樣用Javascript實現函式柯里化與反柯里化
函式柯里化(黑人問號臉)???Currying(黑人問號臉)???妥妥的中式翻譯既視感;下面來一起看看究竟什麼是函式柯里化:
維基百科的解釋是:把接收多個引數的函式變換成接收一個單一引數(最初函式的第一個引數)的函式,並返回接受剩餘的引數而且返回結果的新函式的技術。其由數學家Haskell Brooks Curry提出,並以curry命名。
概念往往都是乾澀且難懂的,讓我們用人話來解釋就是:如果我們不確定這個函式有多少個引數,我們可以先給它傳入一個引數,然後通過js閉包(如若不懂JS閉包,請先學習閉包知識點再來學習本篇博文https://www.cnblogs.com/dengyao-blogs/p/11475575.html)來進行返回一個函式,內部函式接收除開第一個引數外的其餘引數進行操作並輸出,這個就是函式的柯里化;
舉個小例子:
場景(需求):
眾所周知程式設計師每天加班的時間還是比較多的,如果我們需要計算一個程式設計師每天的加班時間,那麼我們的第一反應應該是這樣;
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指向,第二個引數a 程式設計客棧rguments把剩餘引數作為陣列形式新增至空陣列中
[].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){
//執行上面方法
//這裡的this指向下面的s,類似於s(),代表引數長度為0的時候直接呼叫函式
return fn.apply(this,args)
}
console.log(arguments)
//如果arguments的引數長度不為0,即還有剩餘引數
//在陣列的原型物件上新增陣列,apply用來更改this的指向為args
//將[].slice.call(arguments)的陣列新增到原型陣列上
//這裡的[].slice.call(argumenthttp://www.cppcns.coms)===Array.prototype.slice.call(arguments)實質上就是將arguments物件轉成陣列並具有slice功能
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函式柯里化的優點:
- 可以延遲計算,即如果呼叫柯里化函式傳入引數是不呼叫的,會將引數新增到陣列中儲存,等到沒有引數傳入的時候進行呼叫;
- 引數複用,當在多次呼叫同一個函式,並且傳遞的引數絕大多數是相同的,那麼該函式可能是一個很好的柯里化候選。
世間萬物相對,有因必有果,當然了,有柯里化必然有反柯里化;
反柯里化(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.callxarJAhdD(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,3,4] //匿名函式自呼叫並帶入引數1,2,3 })(1,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,4]
在《JavaScript設計模式與開發實踐》一書中,JS函式的反柯里化的案例是這樣寫的:
//定義一個物件
var obj = {
"length":1,"0":1
}
//在Function原型物件定義方法uncurrying
Function.prototype.uncurrying = function() {
//this指向Array.prototype.push
var self = this;
//閉包返回一個內部函式
return function() {
// 這裡可以拆開理解
//首先執行apply return
//Function.prototype.call(Array.prototype.push[obj,2])
//然後Array.prototype.push.call(obj,2)
//call改變指向 obj.push(2)
//所以最後結果就是 {0: 1,1: 2,length: 2}
return Function.prototype.call.apply(self,arguments);
}
}
//在
var push = Array.prototype.push.uncurrying()
push(obj,2)
console.log(obj);
//{0: 1,xarJAhdDlength: 2}
上面的方式不好理解?沒關係,咱們來個好理解的:
Function.prototype.unCurrying = function () { var self = this; return function () { //[].slice.call(arguments,1)===Array.prototype.push.slice.call(arguments,1)===arguments.slice(1) return self.apply(arguments[0],[].slice.call(arguments,1)); }; }; var push = Array.prototype.push.uncurrying() console.log(push); push(obj,2) //{0: 1,length: 2} console.log(obj);
分析一下:
1、首先在Function原型物件上新增uncurrying方法,這樣所有的Function都可以借用;
2、返回一個閉包內部函式
3、閉包函式返回的結果中返回的是呼叫方法,self指向Array.prototype.push,apply方法中第一個引數是更改指向,對應下面push(obj,2)相當於更改指向為obj.push(2)
4、apply方法中第二個引數的call方法是更改指向為arguments,並且arguments中能使用slice方法,等於arguments.slice(1)
以上就是怎樣用Javascript實現函式柯里化與反柯里化的詳細內容,更多關於Javascript函式柯里化與反柯里化的資料請關注我們其它相關文章!