天天看點

(三十八)js之柯裡化

先給大家介紹什麼是柯裡化與反柯裡化 百度翻譯:

  • 在計算機科學中,柯裡化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,并且傳回接受餘下的參數且傳回結果的新函數的技術。這個技術由 Christopher Strachey 以邏輯學家 Haskell Curry 命名的,盡管它是 Moses Schnfinkel 和 Gottlob Frege 發明的。
  • 柯裡化
    • 柯裡化又稱部分求值,其含義是給函數分步傳遞參數,每次傳遞參數後部分應用參數,并傳回一個更具體的函數接受剩下的參數,這中間可嵌套多層這樣的接受部分參數函數,直至傳回最後結果。是以柯裡化的過程是逐漸傳參,逐漸縮小函數的适用範圍,逐漸求解的過程。
    • 另一方面可以說柯裡化是将一個通用性很高的函數變成高是實用性的函數,高通用性的函數必定适用性相對就低了。
    • 比如:你制定了x和y, 如2的3次方,就傳回8, 如果你隻制定x為2,y沒指定, 那麼就傳回一個函數:2的y次方, 這個函數隻有一個參數:y。這樣就非常容易了解吧。
  • 反柯裡化
    • 反柯裡化的作用在與擴大函數的适用性,使本來作為特定對象所擁有的功能的函數可以被任意對象所用.
  • 特點
    • 提高了代碼的合理性,更重的它突出一種思想---降低适用範圍,提高适用性。 對于一個已有函數,對其約定好其中的某些參數輸入,然後生成一個更有好的、更符合業務邏輯的函數。

掌握JavaScript函數的柯裡化

Haskell和 scala都支援函數的柯裡化,JavaScript函數的柯裡化還與 JavaScript的函數程式設計有很大的聯系,如果你感興趣的話,可以在這些方面多下功夫了解,相信收獲一定很多.

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

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

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

?什麼是柯裡化?

我們先來看看 維基百科中是如何定義的:在計算機科學中,柯裡化(英語:Currying),又譯為卡瑞化或加裡化,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,并且傳回接受餘下的參數而且傳回結果的新函數的技術。 我們可以舉個簡單的例子,如下函數add是一般的一個函數,就是将傳進來的參數a和b相加;函數curryingAdd就是對函數add進行柯裡化的函數; 這樣一來,原來我們需要直接傳進去兩個參數來進行運算的函數,現在需要分别傳入參數a和b,函數如下: 看到這裡你可能會想,這樣做有什麼用?為什麼要這樣做?這樣做能夠給我們的應用帶來什麼樣的好處?先别着急,我們接着往下看.

function add(a, b) {
return a + b;
}
 
function curryingAdd(a) {
return function(b) {
return a + b;
}
}
 
add(1, 2); // 3curryingAdd(1)(2); // 3      

?為什麼要對函數進行柯裡化?

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

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

?如何對函數進行柯裡化?

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

  • I 開胃菜 假如我們要實作一個功能,就是輸出語句name喜歡song,其中name和song都是可變參數;那麼一般情況下我們會這樣寫:
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 applebetterShowMsg('dreamapple', 22)('apple'); // My name is dreamapple, I'm 22 years old, and I like eat applebetterShowMsg('dreamapple')(22, 'apple'); // My name is dreamapple, I'm 22 years old, and I like eat applebetterShowMsg('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 applecrazyShowMsg( _, 22, 'apple')('dreamapple'); // My name is dreamapple, I'm 22 years old, and I like eat applecrazyShowMsg( _, 22, _)('dreamapple', _, 'apple'); // My name is dreamapple, I'm 22 years old, and I like eat applecrazyShowMsg( 'dreamapple', _, _)(22)('apple'); // My name is dreamapple, I'm 22 years old, and I like eat applecrazyShowMsg('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); // trueconsole.log(multiply(1)(2)(3)(4)(5) == 120); // true      
        因為multiply(1)(2)(3)的直接結果并不是6,而是一個函數對象{ [Number: 6] valueOf: [Function], toString: [Function] },我們 之後使用了==會将左邊這個函數對象轉換成為一個數字,是以就達到了我們想要的結果.還有關于為什麼使用toString和valueOf方法 可以看看這裡的解釋 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)(); // 6add(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 開胃菜部分的tomLike和jerryLike其實就是屬于這種的,綁定好函數裡面的第一個參數,然後後面根據情況分别使用不同的函數.
  • 固定易變因素 我們經常使用的函數的bind方法就是一個固定易變因素的很好的例子.

?關于柯裡化的性能

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

?關于本章一些知識點的解釋

  • 瑣碎的知識點 fn.length: 表示的是這個函數中參數的個數. arguments.callee: 指向的是目前運作的函數.callee是arguments對象的屬性。 在該函數的函數體内,它可以指向目前正在執行的函數.當函數是匿名函數時,這是很有用的,比如沒有名字的函數表達式(也被叫做"匿名函數"). 詳細解釋可以看這裡 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) = -1partialRight(showMsg, 1)(2, 3); // 實際參數清單: [2, 3, 1] 是以結果是 2 * (3 - 1) = 4partial(showMsg, undefined, 1)(2, 3); // 實際參數清單: [2, 1, 3] 是以結果是 2 * (1 - 3) = -4      
    轉載至:https://www.jianshu.com/p/f02148c64bed?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation(鳴謝)