函數的 柯里化和反柯里化

函數1

函數的柯里化

柯里化(currying)是把接收多個參數的函數變換成爲接收一個部分參數的函數,並返回接收餘下參數的新函數的技術。一般這個參數是一個。
可能咱們對這個解釋不太明白。 如今咱們來思考一個簡單的問題。 怎麼定義一個只有一個參數的函數,實現加法運算。html

function curry(a){ // 建立不銷燬的做用域,保存參數。
    return function(b){ // 返回函數求和
        return a + b;
    }
}

var f = curry(1); // f只是一個函數
console.log(f(2));    // 3

其實咱們把實現加法的函數轉化成上面實例的函數就稱爲函數的柯里化。es6

函數柯里化

柯理化函數思想:一個js預先處理的思想;利用函數執行能夠造成一個不銷燬的做用域的原理,把須要預先處理的內容都儲存在這個不銷燬的做用域中,而且返回一個新函數,之後咱們執行的都是新函數,在新函數中把以前預先存儲的值進行相關的操做處理便可。
並且一般狀況下,這些參數是具備類似性質的。 一般是用於求值。因此函數的柯里化又叫作部分求值。一個currying的函數首先會接受一些參數,接受了這些參數以後,該函數並不會當即求值,而是繼續返回另一個函數,剛纔傳入的參數在函數造成的閉包中被保存起來。待到函數被真正須要求值的時候,以前傳入的全部參數都會被一次性用於求值。
其實就是 f(a,b,c,d) => g(a)(b)(c)(d)() 的過程
或者 f(a,b,c,d) => g(a,b)(c,d)()數組

柯里化的演示

首先給三個數的加法運算。緩存

function add_three(a, b, c){
    return a + b + c;
}

接着按照公式咱們 將 f(a,b,c) ===> g(a)(b)(c)() 的形式。閉包

我首先來改寫成:app

function add(a){ 
    return function(b){
        return function(c){
            return a + b + c;
        }
    }
}

可是這種寫法可能不通用。當參數有不少的時候,可能嵌套的層次就更多了。 其實咱們發現是利用閉包來儲存了變量,那麼咱們換一種思路來存儲參數。咱們在改寫:dom

var add = (function(){
    var args = [];  // 存放參數
    return function(){
        if(arguments.length === 0){  // 無參數就計算
            var sum = 0;
            for(var i = 0; i < args.length; i++){
                sum += args[i]
            }
            return sum;
        }else{  // 有參數就存起來
            [].push.apply(args, arguments);  // 只是存起來了
            return arguments.callee;  // 返回函數,方便鏈式調用
        }
    }
})()

在上面咱們能夠看到柯里化,仍是重寫了一個閉包函數,返回新函數來調用,並且咱們的柯里化函數和被柯里化函數結合很緊密。 那麼咱們可不能夠建立一個函數,這個函數就是用來柯里化其餘函數。這樣就能夠作到解耦的作用了。函數

function currying(fn){
    var args = []; // 存放參數
    return function(){// 返回新函數,也就是柯里化以後的函數
        if(arguments.length === 0){  // 計算
            return fn.apply(fn,args);  // 調用以前的函數
        }else{ //  存儲
            Array.prototype.push.apply(args, arguments);
            return arguments.callee; // 方便鏈式調用 g()()  這樣的形式
        }
    } 
}

function add_three(a, b, c){
    return a + b + c;
}

var add = currying(add_three);
add(1);
console.log(add(2)(3)())

固然爲了更加通用 咱們還能夠改爲學習

function currying(fn, ...arg_init){
    let args = arg_init || [];  // 存儲參數   而且保存 柯里化時傳入的參數。
    return function() {
        if(arguments.length === 0){ // 判斷計算時機
            Let _args = args;
            args = []; // 將參數組置空, 避免重複計算。
            return fn.apply(this, _args); // 調用柯里化的函數
        }else{ // 緩存參數
            [].push.apply(args, arguments);
            return arguments.callee;
        }
    }
}

function add_(...args){
    var sum = 0;
    for(var i = 0; i < args.length; i++){
        sum += args[i];
    }
    return sum;
}

var add = currying(add_three,1);
add(2);
add(3,4);
add(5)(6);
console.log(add())

柯里化的做用

其實,在上面過程當中咱們也看到了,函數柯里化的特色了。this

  1. 參數是具備類似性的。
  2. 緩存數據,須要是在計算。
  3. 能夠將某些參數提早固定。(var add = currying(add_three,1);) 就像這樣的。。提早固定參數

那麼接下來咱們看看柯里化函數有哪些應用場景

  1. 縮小函數適用範圍,提升函數適用性(減小重複傳遞不變的部分參數)
function simpleURL(protocol, domain, path) {
    return protocol + "://" + domain + "/" + path;
}
var myurl = simpleURL('http', 'mysite', 'home.html');
var myurl2 = simpleURL('http', 'mysite', 'aboutme.html');

經過分析
就會發現有時候,咱們在用一些函數,會重複傳遞一些相同的參數,可是咱們又不能由於本身就去改爲函數,那麼咱們就能夠利用柯里化來限制參數,返回新函數來調用。

myURL = currying(simpleURL,"http",'mysite');
myURL('home.html');
myURL('aboutme.html')
  1. 延遲執行
    就是以前求和的案例

  2. 固定異變因素
    提早把易變因素,傳參固定下來,生成一個更明確的應用函數。最典型的表明應用,是bind函數用以固定this這個易變對象。
Function.prototype.bind = function(context) {
    var _this = this;
    var _args = Array.prototype.slice.call(arguments, 1);        
    return function() {
        return _this.apply(context, _args.concat( Array.prototype.slice.call(arguments)));
    }
}

反柯里化

Array.prototype上的方法本來只能用來操做array對象。但用call和apply能夠把任意對象看成this傳入某個方法,這樣一來,方法中用到this的地方就再也不侷限於原來規定的對象,而是加以泛化並獲得更廣的適用性。有沒有辦法把泛化this的過程提取出來呢?反柯里化(uncurrying)就是用來解決這個問題的。反柯里化主要用於擴大適用範圍,建立一個應用範圍更廣的函數。使原本只有特定對象才適用的方法,擴展到更多的對象。
這裏也就是要把obj.fun(arg1, arg2) 轉換成爲 fun1(obj, arg1, arg2) 的形式。其實就是,調用 uncurrying 並傳入一個現有函數 fn, 反柯里化函數會返回一個新函數,該新函數接受的第一個實參將綁定爲 fn 中 this的上下文,其餘參數將傳遞給 fn 做爲參數。

代碼實現以下;

  1. 第一種:
function uncurrying(fn){
    return function(){  // 返回一個 反柯里化函數的新函數
        var args = [].slice.call(arguments,1); // 獲取參數數組, 由於第一參數是咱們傳入的對象自己。
        return fn.apply(arguments[0],args); // 其實本質仍是對象調用本來的函數
    }
}

舉個例子來驗證一下(數組的push)

push = uncurrying(Array.prototype.push);
var arr = [1];
push(arr,2);
console.log(arr); // [1,2]

其實,咱們想要的是第一個參數和後面的參數分割開。 因此上述的反柯里化函數還能夠該改寫成爲:

function uncurrying(fn){
    return function(){  // 返回一個 反柯里化函數的新函數
        var context=[].shift.call(arguments);  // arguments去除第一個參數, 並賦值給新變量
        return fn.apply(context, arguments); // 其實本質仍是對象調用本來的函數
    }
}
  1. 第二種:
    那麼我知道 call 方法和apply方法功能同樣,只不過參數不同。因此能夠寫成:
var uncurrying= function (fn) {
    return function () {        
        return fn.call(...arguments); // es6的語法規則, 自動作參數展開,正好劃分了參數
    }
};
  1. 第三種
    一樣這是你常見的一種形式:(就是call也能夠調用 apply 函數, 作參數的拆分)
var uncurrying= function (fn) {
    return function () {        
        return Function.prototype.call.apply(fn,arguments);
    }
};

因而可知 Function.prototype.call.apply(fn,arguments) 至關於 fn.call(arguments[0],argumnet[1]....)

  1. 第四種
    接下來,咱們知道bind是會返回一個函數,他和call相似,只不過不當即執行而是返回函數。那麼咱們先去內層函數包裹(也就是第一個return),改寫(對第2種):
    咱們知道bind也是具備和call以及apply的性質,並且他還返回一個函數。以前咱們也介紹過了,bind實際上是柯里化的函數,具備指定參數的做用,返回的新函數具備接收剩餘參數的功能。
    針對第二種的改寫(這個是方便你去理解這種形式的。。你能夠參考 bind的 實現源碼來看,上文有介紹)
var uncurrying= function (fn) {
    return fn.call.bind(arguments[0]); // es6的語法規則
};

第三種的改寫,去掉內層函數

// 用函數表達式
var uncurrying = function(fn) {
    return Function.prototype.call.bind(fn);    
};

其實,也就是你常常見到的一種形式, push = Function.prototype.call.bind(Array.prototype.push); 的一層函數封裝。

其實仍是一種你常見的簡潔形式。

  1. 第五種
var uncurrying = Function.prototype.bind.bind(Function.prototype.call);

.
.
.
不解釋了。。。。

反柯里化適用場景。

  1. 讓對象的方法變共有函數, 這樣其餘函數就能夠適用這個方法。 (也就是擴展了函數的適用範圍)
  2. 固定函數反柯里化以後的 this 的指向....this指向的是第一個參數。

總結

其實學習是相互印證的過程,以前也寫過bind的用法,可能仍是停留在一個初級階段,記憶和簡單會用,直到寫完這篇纔算是對bind有了一個深入的認識吧。尤爲是最後的反柯里化函數的簡寫形式,真的是隱藏了不少東西。
阿姨不知道你能不能看明白...

算了仍是解釋下第五種吧:
var uncurrying = Function.prototype.bind.bind(Function.prototype.call);
其實利用bind的源碼實現原理,來去掉最後一個bind
就是變成下面的代碼了,

var uncurrying = function(){
    return Function.prototype.bind.apply(Function.prototype.call,arguments)
}

咱們以前介紹過了apply的特性了,其實就至關於給第一個對象身上綁定了一個方法,調用結束以後刪除該方法。 同理,上述代碼就至關於給call綁定了bind的方法。call去調用bind的方法。 且將agrguments參數展開傳入(這裏也是有細節的), 因此就變成了下面的樣子。

var uncurrying = function(){
    return Function.prototype.call.bind(...arguments);
}

由於咱們傳入的參數是一個,因此就變成這樣了, 是否是就是和第三種同樣了?

var uncurrying = function(fn){
    return Function.prototype.call.bind(fn);
}

接着在去掉bind

var uncurrying = function(fn){
    return function(){
        Function.prototype.call.apply(fn,arguments);
    }
}

也就是(是否是很熟悉了) 這裏之因此能夠這麼寫,利用了一點技巧,偷換了一點概念。

var uncurrying = function(fn){
    return function(){
        fn.call(...arguments);
    }
}
相關文章
相關標籤/搜索