javascript實現(xiàn)函數(shù)柯里化與反柯里化過程解析
函數(shù)柯里化(黑人問號臉)???Currying(黑人問號臉)???妥妥的中式翻譯既視感;下面來一起看看究竟什么是函數(shù)柯里化:
維基百科的解釋是:把接收多個參數(shù)的函數(shù)變換成接收一個單一參數(shù)(最初函數(shù)的第一個參數(shù))的函數(shù),并返回接受剩余的參數(shù)而且返回結果的新函數(shù)的技術。其由數(shù)學家Haskell Brooks Curry提出,并以curry命名。
概念往往都是干澀且難懂的,讓我們用人話來解釋就是:如果我們不確定這個函數(shù)有多少個參數(shù),我們可以先給它傳入一個參數(shù),然后通過JS閉包(如若不懂JS閉包,請先學習閉包知識點再來學習本篇博文http://www.dbjr.com.cn/article/171398.htm)來進行返回一個函數(shù),內(nèi)部函數(shù)接收除開第一個參數(shù)外的其余參數(shù)進行操作并輸出,這個就是函數(shù)的柯里化;
舉個小例子:
場景(需求):
眾所周知程序員每天加班的時間還是比較多的,如果我們需要計算一個程序員每天的加班時間,那么我們的第一反應應該是這樣;
var overtime=0; function time(x){ return overtime+=x; } time(1); //1 time(2); //3 time(3); //6
上面的代碼固然沒有問題,可是需要每天調(diào)用都算加一下當天的時間,很麻煩,并且每調(diào)用一次函數(shù)都要進行一定的操作,如果數(shù)據(jù)量巨大,有可能會有影響性能的風險,那么有沒有可以偷懶又能解決問題的辦法呢?有的!
function time(x){ return function(y){ return x+y; } } var times=time(0); times(3);
但是上面代碼依然存在問題,在實際開發(fā)中很多時候我們的參數(shù)是不確定的,上面代碼雖然簡單的實現(xiàn)了柯里化的基本操作,但是對于參數(shù)不確定的情況是處理不了的;所以存在著函數(shù)參數(shù)的局限性;不過我們從上面的代碼中基本可以知道函數(shù)柯里化是個啥意思了;就是一個函數(shù)調(diào)用的時候只允許傳入一個參數(shù),然后通過閉包返回內(nèi)部函數(shù)去處理和接收剩余參數(shù),返回的函數(shù)通過閉包的方式記住了time的第一個參數(shù);
我們再來把代碼改造一下:
// 首先定義一個變量接收函數(shù) var overtime = (function() { //定義一個數(shù)組用來接收參數(shù) var args = []; //這里運用閉包,調(diào)用外部函數(shù)返回一個內(nèi)部函數(shù) return function() { //arguments是瀏覽器內(nèi)置對象,專門用來接收參數(shù) //如果參數(shù)的長度為0即沒有參數(shù)的時候 if(arguments.length === 0) { //定義變量用來累加 var time = 0; //循環(huán)累加,用i和args的長度進行比較 for (var i = 0, l = args.length; i < l; i++) { //進行累加操作 等價于time=time+args[i] time += args[i]; } // 返回累加的結果 return time; //如果arguments對象參數(shù)長度不為零,即有參數(shù)的時候 }else { //定義的空數(shù)組添加arguments參數(shù)作為數(shù)組項,第一個參數(shù)古args作為改變this指向,第二個參數(shù)arguments把剩余參數(shù)作為數(shù)組形式添加至空數(shù)組中 [].push.apply(args, arguments); } } })(); overtime(3.5); // 第一天 overtime(4.5); // 第二天 overtime(2.1); // 第三天 //... console.log( overtime() ); // 10.1
代碼經(jīng)過我們的改造已經(jīng)實現(xiàn)了功能,但是這不是一個函數(shù)柯里化的完整實現(xiàn),那么我們要怎么完整實現(xiàn)呢?下面我們來介紹一種通用的實現(xiàn)方式:
通用的實現(xiàn)方式:
//定義方法currying,先傳入一個參數(shù) var currying=function(fn){ //定義空數(shù)組裝arguments對象的剩余參數(shù) var args=[]; //利用閉包返回一個函數(shù)處理剩余參數(shù) return function (){ //如果arguments的參數(shù)長度為0,即沒有剩余參數(shù) if(arguments.length===0){ //執(zhí)行上面方法 //這里的this指向下面的s,類似于s(),代表參數(shù)長度為0的時候直接調(diào)用函數(shù) return fn.apply(this,args) } console.log(arguments) //如果arguments的參數(shù)長度不為0,即還有剩余參數(shù) //在數(shù)組的原型對象上添加數(shù)組,apply用來更改this的指向為args //將[].slice.call(arguments)的數(shù)組添加到原型數(shù)組上 //這里的[].slice.call(arguments)===Array.prototype.slice.call(arguments)實質(zhì)上就是將arguments對象轉(zhuǎn)成數(shù)組并具有slice功能 Array.prototype.push.apply(args,[].slice.call(arguments)) //args.push([].slice.call(arguments)) console.log(args) //這里返回的arguments.callee是返回的閉包函數(shù),callee是arguments對象里面的一個屬性,用于返回正被執(zhí)行的function對象 return arguments.callee } } //這里調(diào)用currying方法并傳入add函數(shù),結果會返回閉包內(nèi)部函數(shù) var s=currying(add); //調(diào)用閉包內(nèi)部函數(shù),當有參數(shù)的時候會將參數(shù)逐步添加到args數(shù)組中,待沒有參數(shù)傳入的時候直接調(diào)用 //調(diào)用的時候支持鏈式操作 s(1)(2)(3)(); //也可以一次性傳入多個參數(shù) s(1,2,3); console.log(s());
JS函數(shù)柯里化的優(yōu)點:
1.可以延遲計算,即如果調(diào)用柯里化函數(shù)傳入?yún)?shù)是不調(diào)用的,會將參數(shù)添加到數(shù)組中存儲,等到?jīng)]有參數(shù)傳入的時候進行調(diào)用;
2.參數(shù)復用,當在多次調(diào)用同一個函數(shù),并且傳遞的參數(shù)絕大多數(shù)是相同的,那么該函數(shù)可能是一個很好的柯里化候選。
世間萬物相對,有因必有果,當然了,有柯里化必然有反柯里化;
反柯里化(uncurrying)
從字面意思上來講就是跟柯里化的意思相反;其實真正的反柯里化的作用是擴大適用范圍,就是說當我們調(diào)用某個方法的時候,不需要考慮這個對象自身在設計的過程中有沒有這個方法,只要這個方法適用于它,我們就可以使用;(這里引用的是動態(tài)語言中的鴨子類型的思想)
在學習JS反柯里化之前,我們先學習一下動態(tài)語言的鴨子類型思想,以助于我們更好的理解:
動態(tài)語言鴨子類型思想(維基百科解釋):
在程序設計中,鴨子類型(duck typing)是動態(tài)類型的一種風格。
在這種風格中,一個對象有效的語義,不是由繼承自特定的類或?qū)崿F(xiàn)特定的接口,而是由當前方法和屬性的集合決定。
這個概念的名字來源于由 James Whitcomb Riley 提出的鴨子測試,“鴨子測試”可以這樣表述:
當看到一只鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那么這只鳥就可以被稱為鴨子。
理論上的解釋往往干澀難懂,換成人話來說就是:你是你媽媽的兒子/女兒,不管你是否優(yōu)秀,是否漂亮,只要你是你媽親生的,那么你就是你媽的兒子/女兒;換成鴨子類型就是,只要你會呱呱叫,走起來像鴨子,只要你擁有的行為像鴨子,不管你是不是鴨子,那么你就可以被稱為鴨子;
在Javascript中有很多鴨子類型的引用,比如我們在對一個變量進行賦值的時候,顯然是不需要考慮變量的類型的,正是因為如此,Javascript才更加的靈活,所以Javascript是一門典型的動態(tài)類型語言;
我們來看一下反柯里化中是怎么引用鴨子類型的:
//函數(shù)原型對象上添加uncurring方法 Function.prototype.uncurring = function() { //改變this的指向 //這里的this指向是Array.prototype.push var self = this; //這里的閉包用來返回內(nèi)部函數(shù)的執(zhí)行 return function() { //創(chuàng)建一個變量,在數(shù)組的原型對象上添加shift上面刪除第一個參數(shù) //改變數(shù)組this的指向為arguments var obj = Array.prototype.shift.call(arguments); //最后返回執(zhí)行并給方法改變指向為obj也就是arguments // 并傳入arguments作為參數(shù) return self.apply(obj, arguments); }; }; //數(shù)組原型對象上添加uncurrying方法 var push = Array.prototype.push.uncurring(); //測試一下 //匿名函數(shù)自執(zhí)行 (function() { //這里的push就是一個函數(shù)方法了 //相當于傳入?yún)?shù)arguments和4兩個參數(shù),但是在上面shift方法中刪除第一個參數(shù),這里的arguments參數(shù)被截取了,所以最后實際上只傳入了4 push(arguments, 4); console.log(arguments); //[1, 2, 3, 4] //匿名函數(shù)自調(diào)用并帶入?yún)?shù)1,2,3 })(1, 2, 3)
到這里大家可以想一想arguments是一個接收參數(shù)的對象,里面是沒有push方法的,那么arguments為什么能調(diào)用push方法呢?
這是因為代碼var push = Array.prototype.push.uncurring();在數(shù)組的原型對象的push方法上添加了uncurring方法,然后在執(zhí)行匿名函數(shù)的方法push(arguments, 4);時候?qū)嵸|(zhì)上是在調(diào)用上面的方法在Function的原型對象上添加uncurring方法并返回一個閉包內(nèi)部函數(shù)執(zhí)行,在執(zhí)行的過程中因為Array原型對象上的shift方法會把 push(arguments, 4);中的arguments截取,所以其實方法的實際調(diào)用是push(4),所以最終的結果才是[1,2,3,4]
在《JavaScript設計模式與開發(fā)實踐》一書中,JS函數(shù)的反柯里化的案例是這樣寫的:
//定義一個對象 var obj = { "length":1, "0":1 } //在Function原型對象定義方法uncurrying Function.prototype.uncurrying = function() { //this指向Array.prototype.push var self = this; //閉包返回一個內(nèi)部函數(shù) return function() { // 這里可以拆開理解 //首先執(zhí)行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, 1: 2, length: 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, 1: 2, length: 2} console.log(obj);
分析一下:
1、首先在Function原型對象上添加uncurrying方法,這樣所有的Function都可以借用;
2、返回一個閉包內(nèi)部函數(shù)
3、閉包函數(shù)返回的結果中返回的是調(diào)用方法,self指向Array.prototype.push,apply方法中第一個參數(shù)是更改指向,對應下面push(obj,2)相當于更改指向為obj.push(2)
4、apply方法中第二個參數(shù)的call方法是更改指向為arguments,并且arguments中能使用slice方法,等于arguments.slice(1)
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關文章
js調(diào)用打印機打印網(wǎng)頁字體總是縮小一號的解決方法
直接調(diào)用window.print(),但是打印出來后,字體總是縮小一號,后來直接target="_blank",就可以正常打印了,下面是實現(xiàn)代碼2014-01-01echarts如何實現(xiàn)動態(tài)曲線圖(多條曲線)
這篇文章主要介紹了echarts如何實現(xiàn)動態(tài)曲線圖(多條曲線),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07javascript元素動態(tài)創(chuàng)建實現(xiàn)方法
這篇文章主要介紹了javascript元素動態(tài)創(chuàng)建實現(xiàn)方法,涉及javascript操作元素的相關技巧,需要的朋友可以參考下2015-05-05