1. 程式人生 > >深入理解javascript函數進階系列第二篇——函數柯裏化

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

計算 all urn ray body turn () 通過 null

前面的話

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

定義

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

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

每月開銷函數

  在每天結束之前,都要記錄今天花掉了多少錢。代碼如下:

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

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

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

  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的函數。如果和上面的例子結合,則這個函數的作用是遍歷本月每天的開銷並求出它們的總和

  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()時,才利用前面保存的所有參數,真正開始進行求值計算

可傳參函數

  實際上,柯裏化函數不僅可以接收要柯裏化的函數作為參數,也可以接收一些必要參數,下面是函數柯裏化(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)之後,傳參的同時伴隨著求值的過程,則代碼簡化如下

  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"]

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