柯里化與反柯里化

作者:我是leon
地址:https://juejin.cn/post/6844903645222273037
前言
柯里化,可以理解為提前接收部分參數(shù),延遲執(zhí)行,不立即輸出結(jié)果,而是返回一個(gè)接受剩余參數(shù)的函數(shù)。因?yàn)檫@樣的特性,也被稱為部分計(jì)算函數(shù)。柯里化,是一個(gè)逐步接收參數(shù)的過程。在接下來的剖析中,你會(huì)深刻體會(huì)到這一點(diǎn)。
反柯里化,是一個(gè)泛型化的過程。它使得被反柯里化的函數(shù),可以接收更多參數(shù)。目的是創(chuàng)建一個(gè)更普適性的函數(shù),可以被不同的對象使用。有鳩占鵲巢的效果。
一、柯里化
1.1 例子
實(shí)現(xiàn)?add(1)(2, 3)(4)() = 10?的效果
依題意,有兩個(gè)關(guān)鍵點(diǎn)要注意:
傳入?yún)?shù)時(shí),代碼不執(zhí)行輸出結(jié)果,而是先記憶起來
當(dāng)傳入空的參數(shù)時(shí),代表可以進(jìn)行真正的運(yùn)算
完整代碼如下:
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;});
1.2 記憶傳入?yún)?shù)
由于是延遲計(jì)算結(jié)果,所以要對參數(shù)進(jìn)行記憶。
這里的實(shí)現(xiàn)方式是采用閉包。
function currying(fn){var allArgs = [];return function next(){var args = [].slice.call(arguments);if(args.length > 0){allArgs = allArgs.concat(args);return next;}}}
當(dāng)執(zhí)行var add = currying(...)時(shí),add變量已經(jīng)指向了next方法。此時(shí),allArgs在next方法內(nèi)部有引用到,所以不能被GC回收。也就是說,allArgs在該賦值語句執(zhí)行后,一直存在,形成了閉包。
依靠這個(gè)特性,只要把接收的參數(shù),不斷放入allArgs變量進(jìn)行存儲(chǔ)即可。
所以,當(dāng)arguments.length > 0時(shí),就可以將接收的新參數(shù),放到allArgs中。
最后返回next函數(shù)指針,形成鏈?zhǔn)秸{(diào)用。
1.3 判斷觸發(fā)函數(shù)執(zhí)行條件
題意是,空參數(shù)時(shí),輸出結(jié)果。所以,只要判斷arguments.length == 0即可執(zhí)行。
另外,由于計(jì)算結(jié)果的方法,是作為參數(shù)傳入currying函數(shù),所以要利用apply進(jìn)行執(zhí)行。
綜合上述思考,就可以得到以下完整的柯里化函數(shù)。
function currying(fn){function currying(fn){var allArgs = []; // 用來接收參數(shù)return function next(){var args = [].slice.call(arguments);// 判斷是否執(zhí)行計(jì)算if(args.length > 0){allArgs = allArgs.concat(args); // 收集傳入的參數(shù),進(jìn)行緩存return next;}else{return fn.apply(null, allArgs); // 符合執(zhí)行條件,執(zhí)行計(jì)算}}}
1.4 總結(jié)
柯里化,在這個(gè)例子中可以看出很明顯的行為規(guī)范:
逐步接收參數(shù),并緩存供后期計(jì)算使用
不立即計(jì)算,延后執(zhí)行
符合計(jì)算的條件,將緩存的參數(shù),統(tǒng)一傳遞給執(zhí)行方法
1.5 擴(kuò)展
實(shí)現(xiàn)?add(1)(2, 3)(4)(5) = 15?的效果。
很多人這里就犯嘀咕了:我怎么知道執(zhí)行的時(shí)機(jī)?
其實(shí),這里有個(gè)忍者技藝:valueOf和toString。
js在獲取當(dāng)前變量值的時(shí)候,會(huì)根據(jù)語境,隱式調(diào)用valueOf和toString方法進(jìn)行獲取需要的值。
那么,實(shí)現(xiàn)起來就很簡單了。
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);};// 數(shù)值類型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;});
二、反柯里化
2.1 例子
有以下輕提示類?,F(xiàn)在想要單獨(dú)使用其show方法,輸出新對象obj中的內(nèi)容。
// 輕提示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); // 輸出"新對象"
2.2 反柯里化的行為
非我之物,為我所用
增加被反柯里化方法接收的參數(shù)
在上面的例子中,Toast.prototype.show方法,本來是Toast類的私有方法。跟新對象obj沒有半毛錢關(guān)系。
經(jīng)過反柯里化后,卻可以為obj對象所用。
為什么能被obj所用,是因?yàn)閮?nèi)部將Toast.prototype.show的上下文重新定義為obj。也就是用apply改變了this指向。
而實(shí)現(xiàn)這一步驟的過程,就需要增加反柯里化后的objShow方法參數(shù)。
2.3 另一種反柯里化的實(shí)現(xiàn)
Function.prototype.unCurrying = function(){var self = this;return function(){return Function.prototype.call.apply(self, arguments);}}// 使用var objShow = Toast.prototype.show.unCurrying();objShow(obj);
這里的難點(diǎn),在于理解Function.prototype.call.apply(self, arguments);。
可以分拆為兩步:
1)?Function.prototype.call.apply(...)的解析
可以看成是callFunction.apply(...)。這樣,就清晰很多。callFunction的this指針,被apply修改為self。然后執(zhí)行callFunction?->?callFunction(arguments)
2)?callFunction(arguments)的解析
call方法,第一個(gè)參數(shù),是用來指定this的。所以callFunction(arguments)?->?callFunction(arguments[0], arguments[1-n])。
由此可以得出,反柯里化后,第一個(gè)參數(shù),是用來指定this指向的。
3)為什么要用apply(self, arguments)?如果使用apply(null, arguments),因?yàn)?/span>null對象沒有call方法,會(huì)報(bào)錯(cuò)。
三、實(shí)戰(zhàn)
3.1 判斷變量類型(反柯里化)
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.`);}
3.2 監(jiān)聽事件(柯里化)
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');});
使用柯里化,優(yōu)化監(jiān)聽DOM節(jié)點(diǎn)事件。addEventListener三個(gè)參數(shù)不用每次都寫。
