前言 柯里化,可以理解為 提前接收部分參數,延遲執行,不立即輸出結果,而是返回一個接受剩餘參數的函數 。因為這樣的特性,也被稱為部分計算函數。柯里化,是一個逐步接收參數的過程。在接下來的剖析中,你會深刻體會到這一點。 反柯里化,是一個 泛型化 的過程。它使得被反柯里化的函數,可以 接收更多參數 。目 ...
前言
柯里化,可以理解為提前接收部分參數,延遲執行,不立即輸出結果,而是返回一個接受剩餘參數的函數。因為這樣的特性,也被稱為部分計算函數。柯里化,是一個逐步接收參數的過程。在接下來的剖析中,你會深刻體會到這一點。
反柯里化,是一個泛型化的過程。它使得被反柯里化的函數,可以接收更多參數。目的是創建一個更普適性的函數,可以被不同的對象使用。有鳩占鵲巢的效果。
一、柯里化
1.1 例子
實現 add(1)(2, 3)(4)() = 10
的效果
依題意,有兩個關鍵點要註意:
- 傳入參數時,代碼不執行輸出結果,而是先記憶起來
- 當傳入空的參數時,代表可以進行真正的運算
完整代碼如下:
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 記憶傳入參數
由於是延遲計算結果,所以要對參數進行記憶。
這裡的實現方式是採用閉包。
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
函數指針,形成鏈式調用。
1.3 判斷觸發函數執行條件
題意是,空參數時,輸出結果。所以,只要判斷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); // 符合執行條件,執行計算
}
}
}
1.4 總結
柯里化,在這個例子中可以看出很明顯的行為規範:
- 逐步接收參數,並緩存供後期計算使用
- 不立即計算,延後執行
- 符合計算的條件,將緩存的參數,統一傳遞給執行方法
1.5 擴展
實現 add(1)(2, 3)(4)(5) = 15
的效果。
很多人這裡就犯嘀咕了:我怎麼知道執行的時機?
其實,這裡有個忍者技藝:valueOf
和toString
。
js在獲取當前變數值的時候,會根據語境,隱式調用valueOf
和toString
方法進行獲取需要的值。
那麼,實現起來就很簡單了。
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;
});
二、反柯里化
2.1 例子
有以下輕提示類。現在想要單獨使用其show
方法,輸出新對象obj
中的內容。
// 輕提示
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 反柯里化的行為
- 非我之物,為我所用
- 增加被反柯里化方法接收的參數
在上面的例子中,Toast.prototype.show
方法,本來是Toast
類的私有方法。跟新對象obj
沒有半毛錢關係。
經過反柯里化後,卻可以為obj
對象所用。
為什麼能被obj
所用,是因為內部將Toast.prototype.show
的上下文重新定義為obj
。也就是用apply
改變了this
指向。
而實現這一步驟的過程,就需要增加反柯里化後的objShow
方法參數。
2.3 另一種反柯里化的實現
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
方法,會報錯。
三、實戰
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 監聽事件(柯里化)
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
三個參數不用每次都寫。
後記
其實,反柯里化和泛型方法一樣,只是理念上有一些不同而已。理解這種思維即可。