溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

JavaScript函數(shù)的柯里化是什么意思

發(fā)布時(shí)間:2020-06-29 13:46:46 來(lái)源:億速云 閱讀:368 作者:Leah 欄目:web開發(fā)

JavaScript函數(shù)的柯里化是什么意思?很多新手對(duì)此不是很清楚,為了幫助大家解決這個(gè)難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來(lái)學(xué)習(xí)下,希望你能有所收獲。

什么是柯里化?

我們先來(lái)看看維基百科中是如何定義的:在計(jì)算機(jī)科學(xué)中,柯里化(英語(yǔ):Currying),又譯為卡瑞化或加里化,是把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)(最初函數(shù)的第一個(gè)參數(shù))的函數(shù),并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)。

我們可以舉個(gè)簡(jiǎn)單的例子,如下函數(shù)add是一般的一個(gè)函數(shù),就是將傳進(jìn)來(lái)的參數(shù)ab相加;函數(shù)curryingAdd就是對(duì)函數(shù)add進(jìn)行柯里化的函數(shù);
這樣一來(lái),原來(lái)我們需要直接傳進(jìn)去兩個(gè)參數(shù)來(lái)進(jìn)行運(yùn)算的函數(shù),現(xiàn)在需要分別傳入?yún)?shù)ab,函數(shù)如下:

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

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

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

看到這里你可能會(huì)想,這樣做有什么用?為什么要這樣做?這樣做能夠給我們的應(yīng)用帶來(lái)什么樣的好處?先別著急,我們接著往下看.

為什么要對(duì)函數(shù)進(jìn)行柯里化?
  • 可以使用一些小技巧(見下文)
  • 提前綁定好函數(shù)里面的某些參數(shù),達(dá)到參數(shù)復(fù)用的效果,提高了適用性.
  • 固定易變因素
  • 延遲計(jì)算

總之,函數(shù)的柯里化能夠讓你重新組合你的應(yīng)用,把你的復(fù)雜功能拆分成一個(gè)一個(gè)的小部分,每一個(gè)小的部分都是簡(jiǎn)單的,便于理解的,而且是容易測(cè)試的;

如何對(duì)函數(shù)進(jìn)行柯里化?

在這一部分里,我們由淺入深的一步步來(lái)告訴大家如何對(duì)一個(gè)多參數(shù)的函數(shù)進(jìn)行柯里化.其中用到的知識(shí)有閉包,高階函數(shù),不完全函數(shù)等等.

  • I 開胃菜

    假如我們要實(shí)現(xiàn)一個(gè)功能,就是輸出語(yǔ)句name喜歡song,其中namesong都是可變參數(shù);那么一般情況下我們會(huì)這樣寫:

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

    對(duì)上面的函數(shù)進(jìn)行柯里化之后,我們可以這樣寫:

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

    上面我們雖然對(duì)對(duì)函數(shù)printInfo進(jìn)行了柯里化,但是我們可不想在需要柯里化的時(shí)候,都像上面那樣不斷地進(jìn)行函數(shù)的嵌套,那簡(jiǎn)直是噩夢(mèng);
    所以我們要?jiǎng)?chuàng)造一些幫助其它函數(shù)進(jìn)行柯里化的函數(shù),我們暫且叫它為curryingHelper吧,一個(gè)簡(jiǎn)單的curryingHelper函數(shù)如下所示:

    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);
        }
    }

    這里解釋一點(diǎn)東西,首先函數(shù)的arguments表示的是傳遞到函數(shù)中的參數(shù)對(duì)象,它不是一個(gè)數(shù)組,它是一個(gè)類數(shù)組對(duì)象;
    所以我們可以使用函數(shù)的Array.prototype.slice方法,然后使用.call方法來(lái)獲取arguments里面的內(nèi)容.
    我們使用fn.apply(this, _totalArgs)來(lái)給函數(shù)fn傳遞正確的參數(shù).

    接下來(lái)我們來(lái)寫一個(gè)簡(jiǎn)單的函數(shù)驗(yàn)證上面的輔助柯里化函數(shù)的正確性, 代碼部分如下:

    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

    上面的結(jié)果表示,我們的這個(gè)柯里化的函數(shù)是正確的.上面的curryingHelper就是一個(gè)高階函數(shù),關(guān)于高階函數(shù)的解釋可以參照下文.

  • III 牛肉火鍋

    上面的柯里化幫助函數(shù)確實(shí)已經(jīng)能夠達(dá)到我們的一般性需求了,但是它還不夠好,我們希望那些經(jīng)過(guò)柯里化后的函數(shù)可以每次只傳遞進(jìn)去一個(gè)參數(shù),
    然后可以進(jìn)行多次參數(shù)的傳遞,那么應(yīng)該怎么辦呢?我們可以再花費(fèi)一些腦筋,寫出一個(gè)betterCurryingHelper函數(shù),實(shí)現(xiàn)我們上面說(shuō)的那些
    功能.代碼如下:

    function betterCurryingHelper(fn, len) {
        var length = len || fn.length;
        return function () {
            var allArgsFulfilled = (arguments.length >= length);
    
            // 如果參數(shù)全部滿足,就可以終止遞歸調(diào)用
            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 小雞燉蘑菇中提及的那個(gè)函數(shù).需要注意的是fn.length表示的是這個(gè)函數(shù)的參數(shù)長(zhǎng)度.
    接下來(lái)我們來(lái)檢驗(yàn)一下這個(gè)函數(shù)的正確性:

    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 小雞燉蘑菇部分提及的那個(gè)函數(shù).
    我們可以看出來(lái),這個(gè)betterCurryingHelper確實(shí)實(shí)現(xiàn)了我們想要的那個(gè)功能.并且我們也可以像使用原來(lái)的那個(gè)函數(shù)一樣使用柯里化后的函數(shù).

  • IV 泡椒鳳爪

    我們已經(jīng)能夠?qū)懗龊芎玫目吕锘o助函數(shù)了,但是這還不算是最刺激的,如果我們?cè)趥鬟f參數(shù)的時(shí)候可以不按照順來(lái)那一定很酷;當(dāng)然我們也可以寫出這樣的函數(shù)來(lái),
    這個(gè)crazyCurryingHelper函數(shù)如下所示:

    var _ = {};
    function crazyCurryingHelper(fn, length, args, holes) {
        length = length || fn.length;
        args   = args   || [];
        holes  = holes  || [];
    
        return function() {
            var _args       = args.slice(),
                _holes      = holes.slice();
    
            // 存儲(chǔ)接收到的args和holes的長(zhǎng)度
            var argLength   = _args.length,
                holeLength  = _holes.length;
    
            var allArgumentsSpecified = false;
    
            // 循環(huán)
            var arg     = null,
                i       = 0,
                aLength = arguments.length;
    
            for(; i < aLength; i++) {
                arg = arguments[i];
    
                if(arg === _ && holeLength) {
                    // 循環(huán)holes的位置
                    holeLength--;
                    _holes.push(_holes.shift());
                } else if (arg === _) {
                    // 存儲(chǔ)hole就是_的位置
                    _holes.push(argLength + i);
                } else if (holeLength) {
                    // 是否還有沒(méi)有填補(bǔ)的hole
                    // 在參數(shù)列表指定hole的地方插入當(dāng)前參數(shù)
                    holeLength--;
                    _args.splice(_holes.shift(), 0, arg);
                } else {
                    // 不需要填補(bǔ)hole,直接添加到參數(shù)列表里面
                    _args.push(arg);
                }
            }
    
            // 判斷是否所有的參數(shù)都已滿足
            allArgumentsSpecified = (_args.length >= length);
            if(allArgumentsSpecified) {
                return fn.apply(this, _args);
            }
    
            // 遞歸的進(jìn)行柯里化
            return crazyCurryingHelper.call(this, fn, length, _args, _holes);
        };
    }

    一些解釋,我們使用_來(lái)表示參數(shù)中的那些缺失的參數(shù),如果你使用了lodash的話,會(huì)有沖突的;那么你可以使用別的符號(hào)替代.
    按照一貫的尿性,我們還是要驗(yàn)證一下這個(gè)crazyCurryingHelper是不是實(shí)現(xiàn)了我們所說(shuō)的哪些功能,代碼如下:

    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

    結(jié)果顯示,我們這個(gè)函數(shù)也實(shí)現(xiàn)了我們所說(shuō)的那些功能.

柯里化的一些應(yīng)用場(chǎng)景

說(shuō)了那么多,其實(shí)這部分才是最重要的部分;學(xué)習(xí)某個(gè)知識(shí)要一定可以用得到,不然學(xué)習(xí)它干嘛.

  • 關(guān)于函數(shù)柯里化的一些小技巧

    • setTimeout傳遞地進(jìn)來(lái)的函數(shù)添加參數(shù)

      一般情況下,我們?nèi)绻虢o一個(gè)setTimeout傳遞進(jìn)來(lái)的函數(shù)添加參數(shù)的話,一般會(huì)使用之種方法:

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

      我們使用了一個(gè)新的匿名函數(shù)包裹我們要執(zhí)行的函數(shù),然后在函數(shù)體里面給那個(gè)函數(shù)傳遞參數(shù)值.

      當(dāng)然,在ES5里面,我們也可以使用函數(shù)的bind方法,如下所示:

      setTimeout(hello.bind(this, 'dreamapple'), 3600); // 3.6s 之后執(zhí)行函數(shù)

      這樣也是非常的方便快捷,并且可以綁定函數(shù)執(zhí)行的上下文.

      我們本篇文章是討論函數(shù)的柯里化,當(dāng)然我們這里也可以使用函數(shù)的柯里化來(lái)達(dá)到這個(gè)效果:

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

      這樣也是可以的,是不是很酷.其實(shí)函數(shù)的bind方法也是使用函數(shù)的柯里化來(lái)完成的,詳情可以看這里Function.prototype.bind().

    • 寫出這樣一個(gè)函數(shù)multiply(1)(2)(3) == 6結(jié)果為true,multiply(1)(2)(3)(...)(n) == (1)*(2)*(3)*(...)*(n)結(jié)果為true

      這個(gè)題目不知道大家碰到過(guò)沒(méi)有,不過(guò)通過(guò)函數(shù)的柯里化,也是有辦法解決的,看下面的代碼:

      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

      因?yàn)?code>multiply(1)(2)(3)的直接結(jié)果并不是6,而是一個(gè)函數(shù)對(duì)象{ [Number: 6] valueOf: [Function], toString: [Function] },我們
      之后使用了==會(huì)將左邊這個(gè)函數(shù)對(duì)象轉(zhuǎn)換成為一個(gè)數(shù)字,所以就達(dá)到了我們想要的結(jié)果.還有關(guān)于為什么使用toStringvalueOf方法
      可以看看這里的解釋Number.prototype.valueOf(),Function.prototype.toString().

    • 上面的那個(gè)函數(shù)不夠純粹,我們也可以實(shí)現(xiàn)一個(gè)更純粹的函數(shù),但是可以會(huì)不太符合題目的要求.
      我們可以這樣做,先把函數(shù)的參數(shù)存儲(chǔ),然后再對(duì)這些參數(shù)做處理,一旦有了這個(gè)思路,我們就不難寫出些面的代碼:

      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
    • 當(dāng)我們的需要兼容IE9之前版本的IE瀏覽器的話,我們可能需要寫出一些兼容的方案 ,比如事件監(jiān)聽;一般情況下我們應(yīng)該會(huì)這樣寫:

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

      這也寫也是可以的,但是性能上會(huì)差一點(diǎn),因?yàn)槿绻窃诘桶姹镜腎E瀏覽器上每一次都會(huì)運(yùn)行if()語(yǔ)句,產(chǎn)生了不必要的性能開銷.
      我們也可以這樣寫:

      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);
                  }
              }
          })();

      這樣就減少了不必要的開支,整個(gè)函數(shù)運(yùn)行一次就可以了.

  • 延遲計(jì)算

    上面的那兩個(gè)函數(shù)multiply()add()實(shí)際上就是延遲計(jì)算的例子.

  • 提前綁定好函數(shù)里面的某些參數(shù),達(dá)到參數(shù)復(fù)用的效果,提高了適用性.

    我們的I 開胃菜部分的tomLikejerryLike其實(shí)就是屬于這種的,綁定好函數(shù)里面的第一個(gè)參數(shù),然后后面根據(jù)情況分別使用不同的函數(shù).

  • 固定易變因素

    我們經(jīng)常使用的函數(shù)的bind方法就是一個(gè)固定易變因素的很好的例子.

關(guān)于柯里化的性能

當(dāng)然,使用柯里化意味著有一些額外的開銷;這些開銷一般涉及到這些方面,首先是關(guān)于函數(shù)參數(shù)的調(diào)用,操作arguments對(duì)象通常會(huì)比操作命名的參數(shù)要慢一點(diǎn);
還有,在一些老的版本的瀏覽器中arguments.length的實(shí)現(xiàn)是很慢的;直接調(diào)用函數(shù)fn要比使用fn.apply()或者fn.call()要快一點(diǎn);產(chǎn)生大量的嵌套
作用域還有閉包會(huì)帶來(lái)一些性能還有速度的降低.但是,大多數(shù)的web應(yīng)用的性能瓶頸時(shí)發(fā)生在操作DOM上的,所以上面的那些開銷比起DOM操作的開銷還是比較小的.

關(guān)于本章一些知識(shí)點(diǎn)的解釋
  • 瑣碎的知識(shí)點(diǎn)

    fn.length: 表示的是這個(gè)函數(shù)中參數(shù)的個(gè)數(shù).

    arguments.callee: 指向的是當(dāng)前運(yùn)行的函數(shù).calleearguments對(duì)象的屬性。
    在該函數(shù)的函數(shù)體內(nèi),它可以指向當(dāng)前正在執(zhí)行的函數(shù).當(dāng)函數(shù)是匿名函數(shù)時(shí),這是很有用的,比如沒(méi)有名字的函數(shù)表達(dá)式(也被叫做"匿名函數(shù)").
    詳細(xì)解釋可以看這里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: 返回調(diào)用指定函數(shù)的函數(shù).詳細(xì)的解釋可以看這里Function.caller,下面是示例代碼:

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

    高階函數(shù)就是操作函數(shù)的函數(shù),它接受一個(gè)或多個(gè)函數(shù)作為參數(shù),并返回一個(gè)新的函數(shù).
    我們來(lái)看一個(gè)例子,來(lái)幫助我們理解這個(gè)概念.就舉一個(gè)我們高中經(jīng)常遇到的場(chǎng)景,如下:

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

    我們來(lái)實(shí)現(xiàn)f3函數(shù),看看應(yīng)該如何實(shí)現(xiàn),具體的代碼如下所示:

    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

    我們通過(guò)函數(shù)func3將函數(shù)f1,f2結(jié)合到了一起,然后返回了一個(gè)新的函數(shù)f3;這個(gè)函數(shù)就是我們期望的那個(gè)函數(shù).

  • 不完全函數(shù)(partial function)

    什么是不完全函數(shù)呢?所謂的不完全函數(shù)和我們上面所說(shuō)的柯里化基本差不多;所謂的不完全函數(shù),就是給你想要運(yùn)行的那個(gè)函數(shù)綁定一個(gè)固定的參數(shù)值;
    然后后面的運(yùn)行或者說(shuō)傳遞參數(shù)都是在前面的基礎(chǔ)上進(jìn)行運(yùn)行的.看下面的例子:

    // 一個(gè)將函數(shù)的arguments對(duì)象變成一個(gè)數(shù)組的方法
    function array(a, n) {
        return Array.prototype.slice.call(a, n || 0);
    }
    // 我們要運(yùn)行的函數(shù)
    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); // 打印實(shí)際傳遞到函數(shù)中的參數(shù)列表
            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); // 打印實(shí)際傳遞到函數(shù)中的參數(shù)列表
            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); // 打印實(shí)際傳遞到函數(shù)中的參數(shù)列表
            return f.apply(this, a);
        }
    }
    partialLeft(showMsg, 1)(2, 3); // 實(shí)際參數(shù)列表: [1, 2, 3] 所以結(jié)果是 1 * (2 - 3) = -1
    partialRight(showMsg, 1)(2, 3); // 實(shí)際參數(shù)列表: [2, 3, 1] 所以結(jié)果是 2 * (3 - 1) = 4
    partial(showMsg, undefined, 1)(2, 3); // 實(shí)際參數(shù)列表: [2, 1, 3] 所以結(jié)果是 2 * (1 - 3) = -4

看完上述內(nèi)容是否對(duì)您有幫助呢?如果還想對(duì)相關(guān)知識(shí)有進(jìn)一步的了解或閱讀更多相關(guān)文章,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝您對(duì)億速云的支持。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI