深刻理解javascript函數進階系列第二篇——函數柯里化

前面的話

  函數柯里化currying的概念最先由俄國數學家Moses Schönfinkel發明,然後由著名的數理邏輯學家Haskell Curry將其豐富和發展,currying由此得名。本文將詳細介紹函數柯里化(curring)數組

 

定義

  currying又稱部分求值。一個currying的函數首先會接受一些參數,接受了這些參數以後,該函數並不會當即求值,而是繼續返回另一個函數,剛纔傳入的參數在函數造成的閉包中被保存起來。待到函數被真正須要求值的時候,以前傳入的全部參數都會被一次性用於求值閉包

  從字面上理解currying並不太容易,下面經過編寫一個計算每個月開銷的函數來解釋函數柯里化curryingapp

 

每個月開銷函數

  在天天結束以前,都要記錄今天花掉了多少錢。代碼以下:函數

var monthlyCost = 0;
var cost = function( money ){ 
  monthlyCost += money;
};
cost( 100 ); // 第 1 天開銷 
cost( 200 ); // 第 2 天開銷 
cost( 300 );   // 第 3 天開銷
//...
cost( 700 );   // 第 30 天開銷
alert ( monthlyCost );     // 輸出1個月的總開銷

  天天結束後都會記錄並計算到今天爲止花掉的錢。但其實並不太關心天天花掉了多少錢,而只想知道到月底的時候會花掉多少錢。也就是說,實際上只須要在月底計算一次this

  若是在每月的前29天,都只是保存好當天的開銷,直到最後一天才進行求值計算,這樣就達到了咱們的要求,代碼以下spa

  var cost = (function () {
    var args = [];
    return function () {
      //若是沒有參數,則計算args數組中的和
      if (arguments.length === 0) {
        var money = 0;
        for (var i = 0, l = args.length; i < l; i++) {
          money += args[i];
        }
        return money;
        //若是有參數,則只能是將數據傳到args數組中
      } else {
        [].push.apply(args, arguments);
      }
    }
  })();
  cost(100); // 未真正求值 
  cost(200); // 未真正求值 
  cost(300); // 未真正求值
  console.log(cost()); // 求值並輸出:600

 

通用函數

  下面來編寫一個通用的柯里化函數currying,currying接受一個參數,即將要被currying的函數。若是和上面的例子結合,則這個函數的做用是遍歷本月天天的開銷並求出它們的總和prototype

  var currying = function (fn) {
    var args = [];
    return function () {
      if (arguments.length === 0) {
        return fn.apply(this, args);
      } else {
        [].push.apply(args, arguments);
        return arguments.callee;
      }
    }
  };
  var cost = (function () {
    var money = 0;
    return function () {
      for (var i = 0, l = arguments.length; i < l; i++) {
        money += arguments[i];
      }
      return money;
    }
  })();
  var cost = currying(cost); // 轉化成 currying 函數
  cost(100); // 未真正求值 
  cost(200); // 未真正求值 
  cost(300);   // 未真正求值
  alert(cost());  // 求值並輸出:600

  至此,完成了一個currying函數的編寫。當調用cost()時,若是明確地帶上了一些參數,表示此時並不進行真正的求值計算,而是把這些參數保存起來,此時讓cost函數返回另一個函數。只有以不帶參數的形式執行cost()時,才利用前面保存的全部參數,真正開始進行求值計算code

 

可傳參函數

  實際上,柯里化函數不只能夠接收要柯里化的函數做爲參數,也能夠接收一些必要參數,下面是函數柯里化(currying)的改進代碼對象

  var currying = function (fn) {
    var args = [];
    //儲存傳到curring函數中的除了fn以外的其餘參數,並儲存到args函數中
    args = args.concat([].slice.call(arguments,1));
    return function () {
      if (arguments.length === 0) {
        return fn.apply(this, args);
      } else {
        //將fn中的參數展開,而後再儲存到args數組中
        [].push.apply(args, arguments);
      }
    }
  };
  var cost = (function () {
    var money = 0;
    return function () {
      for (var i = 0, l = arguments.length; i < l; i++) {
        money += arguments[i];
      }
      return money;
    }
  })();
  var cost = currying(cost,100,200); // 轉化成 currying 函數
  cost(100,200); // 未真正求值 
  cost(300);   // 未真正求值
  console.log((cost()));  // 求值並輸出:900

 

求值柯里化

  若是函數柯里化(curring)以後,傳參的同時伴隨着求值的過程,則代碼簡化以下blog

  var currying = function (fn) {
    //獲取除了fn以外的其餘參數
    var args = [].slice.call(arguments, 1);
    return function () {
      //獲取fn裏的全部參數
      var innerArgs = [].slice.call(arguments);
      //最終的參數列表爲args和innerArgs的結合
      var finalArgs = args.concat(innerArgs);
      //將finalArgs裏的參數展開,傳到fn中執行
      return fn.apply(null, finalArgs);
    };
  };
  var cost = (function () {
    var money = 0;
    return function () {
      for (var i = 0, l = arguments.length; i < l; i++) {
        money += arguments[i];
      }
      return money;
    }
  })();
  var cost = currying(cost,100,200); // 轉化成 currying 函數
  cost(300);//100+200+300=600
  cost(100,100);//(100+200+300)+(100+200+100+100)=1100

 

反柯里化

  Array.prototype上的方法本來只能用來操做array對象。但用call和apply能夠把任意對象看成this傳入某個方法,這樣一來,方法中用到this的地方就再也不侷限於原來規定的對象,而是加以泛化並獲得更廣的適用性

  有沒有辦法把泛化this的過程提取出來呢?反柯里化(uncurrying)就是用來解決這個問題的。反柯里化主要用於擴大適用範圍,建立一個應用範圍更廣的函數。使原本只有特定對象才適用的方法,擴展到更多的對象。

  uncurrying的話題來自JavaScript之父Brendan Eich在2011年發表的一篇文章。如下代碼是 uncurrying 的實現方式之一:

Function.prototype.uncurrying = function () { 
  var _this = this;
  return function() {
    var obj = Array.prototype.shift.call( arguments );
    return _this.apply( obj, arguments );
  };
};

  另外一種實現方法以下

Function.prototype.currying = function() {
    var _this = this;
    return function() {
        return Function.prototype.call.apply(_this, arguments);
    }
}

  最終是都把this.method轉化成method(this,arg1,arg2....)以實現方法借用和this的泛化

  下面是一個讓普通對象具有push方法的例子

 var push = Array.prototype.push.uncurrying(),
    obj = {};
  push(obj, 'first', 'two');
  console.log(obj);
/*obj {
    0 : "first",
    1 : "two"
}*/

  經過uncurrying的方式,Array.prototype.push.call變成了一個通用的push函數。這樣一來,push函數的做用就跟Array.prototype.push同樣了,一樣不單單侷限於只能操做array對象。而對於使用者而言,調用push函數的方式也顯得更加簡潔和意圖明瞭

  最後,再看一個例子

var toUpperCase = String.prototype.toUpperCase.uncurrying();
console.log(toUpperCase('avd')); // AVD
function AryUpper(ary) {
    return ary.map(toUpperCase);
}
console.log(AryUpper(['a', 'b', 'c'])); // ["A", "B", "C"]
相關文章
相關標籤/搜索