掌握JavaScript函數的柯里化

原文連接javascript

Haskellscala都支持函數的柯里化,JavaScript函數的柯里化還與JavaScript的函數編程有很大的聯繫,若是你感興趣的話,能夠在這些方面多下功夫瞭解,相信收穫必定不少.html

看本篇文章須要知道的一些知識點前端

  • 函數部分的call/apply/argumentsjava

  • 閉包git

  • 高階函數github

  • 不徹底函數web

文章後面有對這些知識的簡單解釋,你們能夠看看.編程

什麼是柯里化?

咱們先來看看維基百科中是如何定義的:在計算機科學中,柯里化(英語: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

看到這裏你可能會想,這樣作有什麼用?爲何要這樣作?這樣作可以給咱們的應用帶來什麼樣的好處?先彆着急,咱們接着往下看.

爲何要對函數進行柯里化?

  • 可使用一些小技巧(見下文)

  • 提早綁定好函數裏面的某些參數,達到參數複用的效果,提升了適用性.

  • 固定易變因素

  • 延遲計算

總之,函數的柯里化可以讓你從新組合你的應用,把你的複雜功能拆分紅一個一個的小部分,每個小的部分都是簡單的,便於理解的,並且是容易測試的;

如何對函數進行柯里化?

在這一部分裏,咱們由淺入深的一步步來告訴你們如何對一個多參數的函數進行柯里化.其中用到的知識有閉包,高階函數,不徹底函數等等.

  • 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

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

柯里化的一些應用場景

說了那麼多,其實這部分纔是最重要的部分;學習某個知識要必定能夠用獲得,否則學習它幹嗎.

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

    • 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方法就是一個固定易變因素的很好的例子.

關於柯里化的性能

固然,使用柯里化意味着有一些額外的開銷;這些開銷通常涉及到這些方面,首先是關於函數參數的調用,操做arguments對象一般會比操做命名的參數要慢一點;
還有,在一些老的版本的瀏覽器中arguments.length的實現是很慢的;直接調用函數fn要比使用fn.apply()或者fn.call()要快一點;產生大量的嵌套
做用域還有閉包會帶來一些性能還有速度的下降.可是,大多數的web應用的性能瓶頸時發生在操做DOM上的,因此上面的那些開銷比起DOM操做的開銷仍是比較小的.

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

  • 瑣碎的知識點

    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函數式編程的庫,你們能夠看一下:

歡迎提意見:能夠在這裏提意見

參考的資料

相關文章
相關標籤/搜索