1. 程式人生 > >掌握JavaScript函式的柯里化(轉載)

掌握JavaScript函式的柯里化(轉載)

看了之後,突然之間對別的語言有了好奇想去看看了。
– ITDogFire duckywang

Haskellscala都支援函式的柯里化,JavaScript函式的柯里化還與JavaScript的函式程式設計有很大的聯絡,如果你感興趣的話,可以在這些方面多下功夫瞭解,相信收穫一定很多.

:tangerine:看本篇文章需要知道的一些知識點

  • 函式部分的call/apply/arguments
  • 閉包
  • 高階函式
  • 不完全函式

文章後面有對這些知識的簡單解釋,大家可以看看.

:tangerine:什麼是柯里化?

我們先來看看維基百科中是如何定義的:在電腦科學中,柯里化(英語:Currying),又譯為卡瑞化或加里化,是把接受多個引數的函式變換成接受一個單一引數(最初函式的第一個引數)的函式,並且返回接受餘下的引數而且返回結果的新函式的技術。

我們可以舉個簡單的例子,如下函式add是一般的一個函式,就是將傳進來的引數ab相加;函式curryingAdd就是對函式add進行柯里化的函式;
這樣一來,原來我們需要直接傳進去兩個引數來進行運算的函式,現在需要分別傳入引數ab,函式如下:

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

function curryingAdd(a) {
    return function(b) {
        return a + b;
    }
}

add(1, 2); // 3
curryingAdd(1)(2); // 3

看到這裡你可能會想,這樣做有什麼用?為什麼要這樣做?這樣做能夠給我們的應用帶來什麼樣的好處?先彆著急,我們接著往下看.

:tangerine:為什麼要對函式進行柯里化?

  • :apple:可以使用一些小技巧(見下文)
  • :apple:提前繫結好函式裡面的某些引數,達到引數複用的效果,提高了適用性.
  • :apple:固定易變因素
  • :apple:延遲計算

總之,函式的柯里化能夠讓你重新組合你的應用,把你的複雜功能拆分成一個一個的小部分,每一個小的部分都是簡單的,便於理解的,而且是容易測試的;

:tangerine:如何對函式進行柯里化?

在這一部分裡,我們由淺入深的一步步來告訴大家如何對一個多引數的函式進行柯里化.其中用到的知識有閉包,高階函式,不完全函式等等.

  • I 開胃菜

    假如我們要實現一個功能,就是輸出語句name

    喜歡song,其中namesong都是可變引數;那麼一般情況下我們會這樣寫:

    function printInfo(name, song) {
      console.log(name + '喜歡的歌曲是: ' + song);
    }
    printInfo('Tom', '七里香');
    printInfo('Jerry', '雅俗共賞');

    對上面的函式進行柯里化之後,我們可以這樣寫:

    function curryingPrintInfo(name) {
      return function(song) {
          console.log(name + '喜歡的歌曲是: ' + song);
      }
    }
    var tomLike = curryingPrintInfo('Tom');
    tomLike('七里香');
    var jerryLike = curryingPrintInfo('Jerry');
    jerryLike('雅俗共賞');
  • II 小雞燉蘑菇

    上面我們雖然對對函式printInfo進行了柯里化,但是我們可不想在需要柯里化的時候,都像上面那樣不斷地進行函式的巢狀,那簡直是噩夢;
    所以我們要創造一些幫助其它函式進行柯里化的函式,我們暫且叫它為curryingHelper吧,一個簡單的curryingHelper函式如下所示:

    function curryingHelper(fn) {
      var _args = Array.prototype.slice.call(arguments, 1);
      return function() {
          var _newArgs = Array.prototype.slice.call(arguments);
          var _totalArgs = _args.concat(_newArgs);
          return fn.apply(this, _totalArgs);
      }
    }

    這裡解釋一點東西,首先函式的arguments表示的是傳遞到函式中的引數物件,它不是一個數組,它是一個類陣列物件;
    所以我們可以使用函式的Array.prototype.slice方法,然後使用.call方法來獲取arguments裡面的內容.
    我們使用fn.apply(this, _totalArgs)來給函式fn傳遞正確的引數.

    接下來我們來寫一個簡單的函式驗證上面的輔助柯里化函式的正確性, 程式碼部分如下:

    function showMsg(name, age, fruit) {
      console.log('My name is ' + name + ', I\'m ' + age + ' years old, ' + ' and I like eat ' + fruit);
    }
    
    var curryingShowMsg1 = curryingHelper(showMsg, 'dreamapple');
    curryingShowMsg1(22, 'apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    
    var curryingShowMsg2 = curryingHelper(showMsg, 'dreamapple', 20);
    curryingShowMsg2('watermelon'); // My name is dreamapple, I'm 20 years old,  and I like eat watermelon

    上面的結果表示,我們的這個柯里化的函式是正確的.上面的curryingHelper就是一個高階函式,關於高階函式的解釋可以參照下文.

  • III 牛肉火鍋

    上面的柯里化幫助函式確實已經能夠達到我們的一般性需求了,但是它還不夠好,我們希望那些經過柯里化後的函式可以每次只傳遞進去一個引數,
    然後可以進行多次引數的傳遞,那麼應該怎麼辦呢?我們可以再花費一些腦筋,寫出一個betterCurryingHelper函式,實現我們上面說的那些
    功能.程式碼如下:

    function betterCurryingHelper(fn, len) {
      var length = len || fn.length;
      return function () {
          var allArgsFulfilled = (arguments.length >= length);
    
          // 如果引數全部滿足,就可以終止遞迴呼叫
          if (allArgsFulfilled) {
              return fn.apply(this, arguments);
          }
          else {
              var argsNeedFulfilled = [fn].concat(Array.prototype.slice.call(arguments));
              return betterCurryingHelper(curryingHelper.apply(this, argsNeedFulfilled), length - arguments.length);
          }
      };
    }

    其中curryingHelper就是上面II 小雞燉蘑菇中提及的那個函式.需要注意的是fn.length表示的是這個函式的引數長度.
    接下來我們來檢驗一下這個函式的正確性:

    var betterShowMsg = betterCurryingHelper(showMsg);
    betterShowMsg('dreamapple', 22, 'apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    betterShowMsg('dreamapple', 22)('apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    betterShowMsg('dreamapple')(22, 'apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    betterShowMsg('dreamapple')(22)('apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple

    其中showMsg就是II 小雞燉蘑菇部分提及的那個函式.
    我們可以看出來,這個betterCurryingHelper確實實現了我們想要的那個功能.並且我們也可以像使用原來的那個函式一樣使用柯里化後的函式.

  • IV 泡椒鳳爪

    我們已經能夠寫出很好的柯里化輔助函數了,但是這還不算是最刺激的,如果我們在傳遞引數的時候可以不按照順來那一定很酷;當然我們也可以寫出這樣的函式來,
    這個crazyCurryingHelper函式如下所示:

    var _ = {};
    function crazyCurryingHelper(fn, length, args, holes) {
      length = length || fn.length;
      args   = args   || [];
      holes  = holes  || [];
    
      return function() {
          var _args       = args.slice(),
              _holes      = holes.slice();
    
          // 儲存接收到的args和holes的長度
          var argLength   = _args.length,
              holeLength  = _holes.length;
    
          var allArgumentsSpecified = false;
    
          // 迴圈
          var arg     = null,
              i       = 0,
              aLength = arguments.length;
    
          for(; i < aLength; i++) {
              arg = arguments[i];
    
              if(arg === _ && holeLength) {
                  // 迴圈holes的位置
                  holeLength--;
                  _holes.push(_holes.shift());
              } else if (arg === _) {
                  // 儲存hole就是_的位置
                  _holes.push(argLength + i);
              } else if (holeLength) {
                  // 是否還有沒有填補的hole
                  // 在引數列表指定hole的地方插入當前引數
                  holeLength--;
                  _args.splice(_holes.shift(), 0, arg);
              } else {
                  // 不需要填補hole,直接新增到引數列表裡面
                  _args.push(arg);
              }
          }
    
          // 判斷是否所有的引數都已滿足
          allArgumentsSpecified = (_args.length >= length);
          if(allArgumentsSpecified) {
              return fn.apply(this, _args);
          }
    
          // 遞迴的進行柯里化
          return crazyCurryingHelper.call(this, fn, length, _args, _holes);
      };
    }

    一些解釋,我們使用_來表示引數中的那些缺失的引數,如果你使用了lodash的話,會有衝突的;那麼你可以使用別的符號替代.
    按照一貫的尿性,我們還是要驗證一下這個crazyCurryingHelper是不是實現了我們所說的哪些功能,程式碼如下:

    var crazyShowMsg = crazyCurryingHelper(showMsg);
    crazyShowMsg(_, 22)('dreamapple')('apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    crazyShowMsg( _, 22, 'apple')('dreamapple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    crazyShowMsg( _, 22, _)('dreamapple', _, 'apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    crazyShowMsg( 'dreamapple', _, _)(22)('apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple
    crazyShowMsg('dreamapple')(22)('apple'); // My name is dreamapple, I'm 22 years old,  and I like eat apple

    結果顯示,我們這個函式也實現了我們所說的那些功能.

:tangerine:柯里化的一些應用場景

說了那麼多,其實這部分才是最重要的部分;學習某個知識要一定可以用得到,不然學習它幹嘛:joy:

  • 關於函式柯里化的一些小技巧

    • setTimeout傳遞地進來的函式新增引數

      一般情況下,我們如果想給一個setTimeout傳遞進來的函式新增引數的話,一般會使用這種方法:

      function hello(name) {
          console.log('Hello, ' + name);
      }
      setTimeout(hello('dreamapple'), 3600); //立即執行,不會在3.6s後執行
      setTimeout(function() {
          hello('dreamapple');
      }, 3600); // 3.6s 後執行

      我們使用了一個新的匿名函式包裹我們要執行的函式,然後在函式體裡面給那個函式傳遞引數值.

      當然,在ES5裡面,我們也可以使用函式的bind方法,如下所示:

      setTimeout(hello.bind(this, 'dreamapple'), 3600); // 3.6s 之後執行函式

      這樣也是非常的方便快捷,並且可以繫結函式執行的上下文.

      我們本篇文章是討論函式的柯里化,當然我們這裡也可以使用函式的柯里化來達到這個效果:

      setTimeout(curryingHelper(hello, 'dreamapple'), 3600); // 其中curryingHelper是上面已經提及過的

      這樣也是可以的,是不是很酷.其實函式的bind方法也是使用函式的柯里化來完成的,詳情可以看這裡Function.prototype.bind().

    • 寫出這樣一個函式multiply(1)(2)(3) == 6結果為true,multiply(1)(2)(3)(...)(n) == (1)*(2)*(3)*(...)*(n)結果為true

      這個題目不知道大家碰到過沒有,不過通過函式的柯里化,也是有辦法解決的,看下面的程式碼:

      function multiply(x) {
        var y = function(x) {
            return multiply(x * y);
        };
        y.toString = y.valueOf = function() {
            return x;
        };
        return y;
      }
      
      console.log(multiply(1)(2)(3) == 6); // true
      console.log(multiply(1)(2)(3)(4)(5) == 120); // true

      因為multiply(1)(2)(3)的直接結果並不是6,而是一個函式物件{ [Number: 6] valueOf: [Function], toString: [Function] },我們
      之後使用了==會將左邊這個函式物件轉換成為一個數字,所以就達到了我們想要的結果.還有關於為什麼使用toStringvalueOf方法
      可以看看這裡的解釋Number.prototype.valueOf(),Function.prototype.toString().

    • 上面的那個函式不夠純粹,我們也可以實現一個更純粹的函式,但是可以會不太符合題目的要求.
      我們可以這樣做,先把函式的引數儲存,然後再對這些引數做處理,一旦有了這個思路,我們就不難寫出些面的程式碼:

      function add() {
        var args = Array.prototype.slice.call(arguments);
        var _that = this;
        return function() {
            var newArgs = Array.prototype.slice.call(arguments);
            var total = args.concat(newArgs);
            if(!arguments.length) {
                var result = 1;
                for(var i = 0; i < total.length; i++) {
                    result *= total[i];
                }
                return result;
            }
            else {
                return add.apply(_that, total);
            }
        }
      }
      add(1)(2)(3)(); // 6
      add(1, 2, 3)(); // 6
    • 當我們的需要相容IE9之前版本的IE瀏覽器的話,我們可能需要寫出一些相容的方案 ,比如事件監聽;一般情況下我們應該會這樣寫:

      var addEvent = function (el, type, fn, capture) {
            if (window.addEventListener) {
                el.addEventListener(type, fn, capture);
            }
            else {
                el.attachEvent('on' + type, fn);
            }
        };

      這也寫也是可以的,但是效能上會差一點,因為如果是在低版本的IE瀏覽器上每一次都會執行if()語句,產生了不必要的效能開銷.
      我們也可以這樣寫:

      var addEvent = (function () {
            if (window.addEventListener) {
                return function (el, type, fn, capture) {
                    el.addEventListener(type, fn, capture);
                }
            }
            else {
                return function (el, type, fn) {
                    var IEtype = 'on' + type;
                    el.attachEvent(IEtype, fn);
                }
            }
        })();

      這樣就減少了不必要的開支,整個函式執行一次就可以了.

  • 延遲計算

    上面的那兩個函式multiply()add()實際上就是延遲計算的例子.

  • 提前繫結好函式裡面的某些引數,達到引數複用的效果,提高了適用性.

    我們的I 開胃菜部分的tomLikejerryLike其實就是屬於這種的,繫結好函式裡面的第一個引數,然後後面根據情況分別使用不同的函式.

  • 固定易變因素

    我們經常使用的函式的bind方法就是一個固定易變因素的很好的例子.

:tangerine:關於柯里化的效能

當然,使用柯里化意味著有一些額外的開銷;這些開銷一般涉及到這些方面,首先是關於函式引數的呼叫,操作arguments物件通常會比操作命名的引數要慢一點;
還有,在一些老的版本的瀏覽器中arguments.length的實現是很慢的;直接呼叫函式fn要比使用fn.apply()或者fn.call()要快一點;產生大量的巢狀
作用域還有閉包會帶來一些效能還有速度的降低.但是,大多數的web應用的效能瓶頸時發生在操作DOM上的,所以上面的那些開銷比起DOM操作的開銷還是比較小的.

:tangerine:關於本章一些知識點的解釋

  • 瑣碎的知識點

    fn.length: 表示的是這個函式中引數的個數.

    arguments.callee: 指向的是當前執行的函式.calleearguments物件的屬性。
    在該函式的函式體內,它可以指向當前正在執行的函式.當函式是匿名函式時,這是很有用的,比如沒有名字的函式表示式(也被叫做”匿名函式”).
    詳細解釋可以看這裡arguments.callee.我們可以看一下下面的例子:

    function hello() {
      return function() {
          console.log('hello');
          if(!arguments.length) {
              console.log('from a anonymous function.');
              return arguments.callee;
          }
      }
    }
    
    hello()(1); // hello
    
    /*
    * hello
    * from a anonymous function.
    * hello
    * from a anonymous function.
    */
    hello()()();

    fn.caller: 返回呼叫指定函式的函式.詳細的解釋可以看這裡Function.caller,下面是示例程式碼:

    function hello() {
      console.log('hello');
      console.log(hello.caller);
    }
    
    function callHello(fn) {
      return fn();
    }
    
    callHello(hello); // hello [Function: callHello]
  • 高階函式(high-order function)

    高階函式就是操作函式的函式,它接受一個或多個函式作為引數,並返回一個新的函式.
    我們來看一個例子,來幫助我們理解這個概念.就舉一個我們高中經常遇到的場景,如下:

    f1(x, y) = x + y;
    f2(x) = x * x;
    f3 = f2(f3(x, y));

    我們來實現f3函式,看看應該如何實現,具體的程式碼如下所示:

    function f1(x, y) {
      return x + y;
    }
    
    function f2(x) {
      return x * x;
    }
    
    function func3(func1, func2) {
      return function() {
          return func2.call(this, func1.apply(this, arguments));
      }
    }
    
    var f3 = func3(f1, f2);
    console.log(f3(2, 3)); // 25

    我們通過函式func3將函式f1,f2結合到了一起,然後返回了一個新的函式f3;這個函式就是我們期望的那個函式.

  • 不完全函式(partial function)

    什麼是不完全函式呢?所謂的不完全函式和我們上面所說的柯里化基本差不多;所謂的不完全函式,就是給你想要執行的那個函式繫結一個固定的引數值;
    然後後面的執行或者說傳遞引數都是在前面的基礎上進行執行的.看下面的例子:

    // 一個將函式的arguments物件變成一個數組的方法
    function array(a, n) {
      return Array.prototype.slice.call(a, n || 0);
    }
    // 我們要執行的函式
    function showMsg(a, b, c){
      return a * (b - c);
    }
    
    function partialLeft(f) {
      var args = arguments;
      return function() {
          var a = array(args, 1);
          a = a.concat(array(arguments));
          console.log(a); // 列印實際傳遞到函式中的引數列表
          return f.apply(this, a);
      }
    }
    
    function partialRight(f) {
      var args = arguments;
      return function() {
          var a = array(arguments);
          a = a.concat(array(args, 1));
          console.log(a); // 列印實際傳遞到函式中的引數列表
          return f.apply(this, a);
      }
    }
    
    function partial(f) {
      var args = arguments;
      return function() {
          var a = array(args, 1);
          var i = 0; j = 0;
          for(; i < a.length; i++) {
              if(a[i] === undefined) {
                  a[i] = arguments[j++];
              }
          }
          a = a.concat(array(arguments, j));
          console.log(a); // 列印實際傳遞到函式中的引數列表
          return f.apply(this, a);
      }
    }
    
    
    partialLeft(showMsg, 1)(2, 3); // 實際引數列表: [1, 2, 3] 所以結果是 1 * (2 - 3) = -1
    partialRight(showMsg, 1)(2, 3); // 實際引數列表: [2, 3, 1] 所以結果是 2 * (3 - 1) = 4
    partial(showMsg, undefined, 1)(2, 3); // 實際引數列表: [2, 1, 3] 所以結果是 2 * (1 - 3) = -4

一些你可能會喜歡的JS庫

JavaScript的柯里化與JavaScript的函數語言程式設計密不可分,下面列舉了一些關於JavaScript函數語言程式設計的庫,大家可以看一下:

歡迎提意見

參考的資料