柯里化與反柯里化

前言

柯里化,能夠理解爲提早接收部分參數,延遲執行,不當即輸出結果,而是返回一個接受剩餘參數的函數。由於這樣的特性,也被稱爲部分計算函數。柯里化,是一個逐步接收參數的過程。在接下來的剖析中,你會深入體會到這一點。javascript

反柯里化,是一個泛型化的過程。它使得被反柯里化的函數,能夠接收更多參數。目的是建立一個更普適性的函數,能夠被不一樣的對象使用。有鳩佔鵲巢的效果。java

1、柯里化

1.1 例子

實現 add(1)(2, 3)(4)() = 10 的效果node

依題意,有兩個關鍵點要注意:git

  • 傳入參數時,代碼不執行輸出結果,而是先記憶起來
  • 當傳入空的參數時,表明能夠進行真正的運算

完整代碼以下:github

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方法。此時,allArgsnext方法內部有引用到,因此不能被GC回收。也就是說,allArgs在該賦值語句執行後,一直存在,造成了閉包。
依靠這個特性,只要把接收的參數,不斷放入allArgs變量進行存儲便可。
因此,當arguments.length > 0時,就能夠將接收的新參數,放到allArgs中。
最後返回next函數指針,造成鏈式調用。閉包

1.3 判斷觸發函數執行條件

題意是,空參數時,輸出結果。因此,只要判斷arguments.length == 0便可執行。
另外,因爲計算結果的方法,是做爲參數傳入currying函數,因此要利用apply進行執行。
綜合上述思考,就能夠獲得如下完整的柯里化函數。app

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 的效果。
不少人這裏就犯嘀咕了:我怎麼知道執行的時機?
其實,這裏有個忍者技藝:valueOftoString
js在獲取當前變量值的時候,會根據語境,隱式調用valueOftoString方法進行獲取須要的值。
那麼,實現起來就很簡單了。優化

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、反柯里化

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(...)。這樣,就清晰不少。
callFunctionthis指針,被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、實戰

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三個參數不用每次都寫。

後記

其實,反柯里化和泛型方法同樣,只是理念上有一些不一樣而已。理解這種思惟便可。


喜歡我文章的朋友,能夠經過如下方式關注我:

相關文章
相關標籤/搜索