柯里化,能夠理解爲 提早接收部分參數,延遲執行,不當即輸出結果,而是返回一個接受剩餘參數的函數。由於這樣的特性,也被稱爲部分計算函數。柯里化,是一個逐步接收參數的過程。在接下來的剖析中,你會深入體會到這一點。反柯里化,是一個泛型化的過程。它使得被反柯里化的函數,能夠接收更多參數。目的是建立一個更普適性的函數,能夠被不一樣的對象使用。有鳩佔鵲巢的效果。javascript
實現 add(1)(2, 3)(4)() = 10
的效果java
依題意,有兩個關鍵點要注意:node
- 傳入參數時,代碼不執行輸出結果,而是先記憶起來
- 當傳入空的參數時,表明能夠進行真正的運算
完整代碼以下:緩存
function currying(fn){ var allArgs = []; return function next(){ var args = [].slice.call(arguments); if(args.length > 0){ allArgs = allArgs.concat(args); return next; }else{ return fn.apply(null, allArgs); } } } var add = currying(function(){ var sum = 0; for(var i = 0; i < arguments.length; i++){ sum += arguments[i]; } return sum; });
因爲是延遲計算結果,因此要對參數進行記憶。
這裏的實現方式是採用閉包。閉包
function currying(fn){ var allArgs = []; return function next(){ var args = [].slice.call(arguments); if(args.length > 0){ allArgs = allArgs.concat(args); return next; } } }
當執行var add = currying(...)
時,add
變量已經指向了next
方法。此時,allArgs
在next
方法內部有引用到,因此不能被GC回收。也就是說,allArgs
在該賦值語句執行後,一直存在,造成了閉包。
依靠這個特性,只要把接收的參數,不斷放入allArgs
變量進行存儲便可。
因此,當arguments.length > 0
時,就能夠將接收的新參數,放到allArgs
中。
最後返回next
函數指針,造成鏈式調用。app
題意是,空參數時,輸出結果。因此,只要判斷arguments.length == 0
便可執行。
另外,因爲計算結果的方法,是做爲參數傳入currying
函數,因此要利用apply
進行執行。
綜合上述思考,就能夠獲得如下完整的柯里化函數。函數
function currying(fn){ var allArgs = []; // 用來接收參數 return function next(){ var args = [].slice.call(arguments); // 判斷是否執行計算 if(args.length > 0){ allArgs = allArgs.concat(args); // 收集傳入的參數,進行緩存 return next; }else{ return fn.apply(null, allArgs); // 符合執行條件,執行計算 } } }
柯里化,在這個例子中能夠看出很明顯的行爲規範:優化
實現 add(1)(2, 3)(4)(5) = 15
的效果。
不少人這裏就犯嘀咕了:我怎麼知道執行的時機?
其實,這裏有個忍者技藝:valueOf
和toString
。
js在獲取當前變量值的時候,會根據語境,隱式調用valueOf
和toString
方法進行獲取須要的值。
那麼,實現起來就很簡單了。this
function currying(fn){ var allArgs = []; function next(){ var args = [].slice.call(arguments); allArgs = allArgs.concat(args); return next; } // 字符類型 next.toString = function(){ return fn.apply(null, allArgs); }; // 數值類型 next.valueOf = function(){ return fn.apply(null, allArgs); } return next; } var add = currying(function(){ var sum = 0; for(var i = 0; i < arguments.length; i++){ sum += arguments[i]; } return sum; });
有如下輕提示類。如今想要單獨使用其show
方法,輸出新對象obj
中的內容。prototype
// 輕提示 function Toast(option){ this.prompt = ''; } Toast.prototype = { constructor: Toast, // 輸出提示 show: function(){ console.log(this.prompt); } }; // 新對象 var obj = { prompt: '新對象' };
用反柯里化的方式,能夠這麼作
function unCurrying(fn){ return function(){ var args = [].slice.call(arguments); var that = args.shift(); return fn.apply(that, args); } } var objShow = unCurrying(Toast.prototype.show); objShow(obj); // 輸出"新對象"
在上面的例子中,Toast.prototype.show
方法,原本是Toast
類的私有方法。跟新對象obj
沒有半毛錢關係。
通過反柯里化後,卻能夠爲obj
對象所用。
爲何能被obj
所用,是由於內部將Toast.prototype.show
的上下文從新定義爲obj
。也就是用apply
改變了this
指向。
而實現這一步驟的過程,就須要增長反柯里化後的objShow
方法參數。
Function.prototype.unCurrying = function(){ var self = this; return function(){ return Function.prototype.call.apply(self, arguments); } } // 使用 var objShow = Toast.prototype.show.unCurrying(); objShow(obj);
這裏的難點,在於理解Function.prototype.call.apply(self, arguments);
。
能夠分拆爲兩步:
1) Function.prototype.call.apply(...)
的解析
能夠當作是callFunction.apply(...)
。這樣,就清晰不少。 callFunction
的this
指針,被apply
修改成self
。
而後執行callFunction
-> callFunction(arguments)
2) callFunction(arguments)
的解析
call
方法,第一個參數,是用來指定this
的。因此callFunction(arguments)
-> callFunction(arguments[0], arguments[1-n])
。
由此能夠得出,反柯里化後,第一個參數,是用來指定this
指向的。
3)爲何要用apply(self, arguments)
若是使用apply(null, arguments)
,由於null
對象沒有call
方法,會報錯。
var fn = function(){}; var val = 1; if(Object.prototype.toString.call(fn) == '[object Function]'){ console.log(`${fn} is function.`); } if(Object.prototype.toString.call(val) == '[object Number]'){ console.log(`${val} is number.`); }
上述代碼,用反柯里化,能夠這麼寫:
var fn = function(){}; var val = 1; var toString = Object.prototype.toString.unCurrying(); if(toString(fn) == '[object Function]'){ console.log(`${fn} is function.`); } if(toString(val) == '[object Number]'){ console.log(`${val} is number.`); }
function nodeListen(node, eventName){ return function(fn){ node.addEventListener(eventName, function(){ fn.apply(this, Array.prototype.slice.call(arguments)); }, false); } } var bodyClickListen = nodeListen(document.body, 'click'); bodyClickListen(function(){ console.log('first listen'); }); bodyClickListen(function(){ console.log('second listen'); });
使用柯里化,優化監聽DOM節點事件。addEventListener
三個參數不用每次都寫。
其實,反柯里化和泛型方法同樣,只是理念上有一些不一樣而已。理解這種思惟便可。