理解JS裏的偏函數與柯里化

  聯繫到上篇博客講的bind完整的語法爲:瀏覽器

let bound = func.bind(context, arg1, arg2, ...);

  能夠綁定上下文this和函數的初始參數。舉例,咱們有個乘法函數mul(a,b):app

function mul(a, b) { return a * b; }

  咱們能夠在該函數的基礎上使用綁定建立一個double函數:函數

let double = mul.bind(null, 2); alert( double(3) ); // = mul(2, 3) = 6

  調用mul.bind(null, 2)建立新函數double,傳遞調用mul函數,固定第一個參數上下文爲null,第二個參數爲2,多個參數傳遞也是如此。this

  這稱爲偏函數應用——咱們創造一個新函數,讓現有的一些參數值固定。lua

  注意,這裏確實不用this,但bind須要,因此必須使用null。spa

  爲何咱們一般使用偏函數?翻譯

  這裏咱們偏函數的好處是:code

  (1)經過建立一個名稱易懂的獨立函數(double,triple等),調用時無需每次傳入第一個參數,由於第一個參數經過bind提供了固定值blog

  (2)另外一種使用偏函數狀況是,當咱們有一個很通用的函數,爲了方便提供一個較經常使用的變體。舉例,咱們有一個函數send(from, to, text),那麼使用偏函數能夠建立一個從當前用戶發送的變體:sendTo(to, text)事件

偏函數與柯里化定義:

  維基百科中對偏函數 (Partial application) 的定義爲:

In computer science, partial application (or partial function application) 
refers to the process of fixing a number of arguments to a function,

producing another function of smaller arity.

  翻譯成中文:在計算機科學中,局部應用是指固定一個函數的一些參數,而後產生另外一個更小元的函數。(什麼是元?元是指函數參數的個數,好比一個帶有兩個參數的函數被稱爲二元函數。)

  維基百科中對柯里化 (Currying) 的定義爲:

In mathematics and computer science, 
currying is the technique of translating the evaluation of a function
that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions,

each with a single argument.

  翻譯成中文:在數學和計算機科學中,柯里化是一種將使用多個參數的一個函數轉換成一系列使用一個參數的函數的技術。

  偏函數與柯里化區別:

  柯里化是將一個多參數函數轉換成多個單參數函數,也就是將一個 n 元函數轉換成 n 個一元函數。

  局部應用則是固定一個函數的一個或者多個參數,也就是將一個 n 元函數轉換成一個 n - x 元函數。

使用沒有上下文的偏函數

  bind能夠實現偏函數應用,可是若是想固定一些參數,但不綁定this呢?

  內置的bind不容許這樣,咱們不能忽略上下文並跳轉到參數。幸運的是,能夠僅綁定參數partial函數容易實現。以下:

function partial(func, ...argsBound) { return function(...args) { return func.call(this, ...argsBound, ...args); } } let user = { firstName: "John", say(time, phrase) { alert(`[${time}] ${this.firstName}: ${phrase}!`); } }; // 偏函數,綁定第一個參數,say的time
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); //調用新函數提供第二個參數phrase
user.sayNow("Hello"); // [10:00] Hello, John!

  調用partial(func[, arg1, arg2...])函數的結果爲調用func的包裝器(即第一個return的函數):

  (1)this一致(由於user.sayNow是經過user調用的)

  (2)而後給其...argsBound—— partial使用該參數("10:00")進行調用。

  (3)而後提供參數...args——提供給包裝器的參數(「Hello「)

  因此使用spread運算符很容易實現。

柯里化實現

  有時人們混淆上面說起的偏函數和「柯里化」函數功能,柯里化是另外一個有趣的處理函數技術。柯里化(Currying):轉換一個調用函數f(a,b,c)f(a)(b)(c)方式調用。讓咱們實現柯里化函數,執行一個兩元參數函數,即轉換f(a,b)f(a)(b):

function curry(func) { return function(a) { return function(b) { return func(a, b); }; }; } // usage
function sum(a, b) { return a + b; } let carriedSum = curry(sum); alert( carriedSum(1)(2) ); // 3

  上面是經過一系列包裝器實現的。

  (1)curry(func)的結果是function(a)的一個包裝器。

  (2)當調用sum(1)是,參數被保存在詞法環境中,而後返回新的包裝器function(b)

  (3)而後sum(1)(2)提供2並最終調用function(b),而後傳遞調用給原始多參數函數sum

高級柯里化實現

  有一些柯里化的高級實現,能夠實現更復雜功能:其返回一個包裝器,它容許函數提供所有參數被正常調用,或返回偏函數。實現以下:

function curry(func) { return function curried(...args) { if (args.length >= func.length) {//若是參數大於等於函數參數,那麼容許函數提供所有參數被正常調用
      return func.apply(this, args); } else {//提供參數小於函數參數,返回偏函數
      return function pass(...args2) { return curried.apply(this, args.concat(args2)); } } }; } function sum(a, b, c) { return a + b + c; } let curriedSum = curry(sum); // 提供所有參數,正常調用
alert( curriedSum(1, 2, 3) ); // 6 // 返回偏函數包裝器,並提供二、3參數
alert( curriedSum(1)(2,3) ); // 6

  當咱們運行時,有兩個分支:

  一、提供所有參數正常調用:若是傳遞args數與原函數已經定義的參數個數同樣或更長,那麼直接調用。

  二、得到偏函數:不然,不調用func函數,返回另外一個包裝器,提供鏈接以前的參數一塊兒作爲新參數從新應用curried。而後再次執行一個新調用,返回一個新偏函數(若是參數不夠)或最終結果。

  舉例,讓咱們看sum(a, b, c)會怎樣,三個參數,因此sum.length=3;

  若是調用curried(1)(2)(3):

  (1)第一次調用curried(1),在詞法環境中記住1,返回包裝器pass;

  (2)使用參數2調用包裝器pass:其帶着前面的參數1,鏈接他們而後調用curried(1,2),由於參數數量仍然小於3,返回包裝器pass;

  (3)再次使用參數3調用包裝器pass,帶着以前的參數(1,2),而後增長3,並調用curried(1,2,3)——最終有三個參數,傳遞給原始函數,而後參數個數相等,就直接調用func函數。

總結

  一、當把已知函數的一些參數固定,結果函數被稱爲偏函數。經過使用bind得到偏函數,也有其餘方式實現。

  用途:當咱們不想一次一次重複相同的參數時,偏函數是很便捷的。如咱們有send(from,to)函數,若是from老是相同的,可使用偏函數簡化調用。

  二、柯里化是轉換函數調用從f(a,b,c)f(a)(b)(c),Javascript一般既實現正常調用,也實現參數數量不足時的偏函數方式調用。

  用途:(1)參數複用;(2)提早返回;(3)延遲計算或運行,參數隨意設置。

  這裏說一下「提早返回」,很常見的一個例子:兼容現代瀏覽器以及IE瀏覽器的事件添加方法。咱們正常狀況可能會這樣寫:

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

  上面的方法有什麼問題呢?很顯然,咱們每次使用addEvent爲元素添加事件的時候,(eg. IE6/IE7)都會走一遍if...else if ...其實只要一次斷定就能夠了,怎麼作?——柯里化。改成下面這樣子的代碼:

var addEvent = (function(){ if (window.addEventListener) { return function(el, sType, fn, capture) { el.addEventListener(sType, function(e) { fn.call(el, e); }, (capture)); }; } else if (window.attachEvent) { return function(el, sType, fn, capture) { el.attachEvent("on" + sType, function(e) { fn.call(el, e); }); }; } })();

  初始addEvent的執行其實只實現了部分的應用(只有一次的if...else if...斷定),而剩餘的參數應用都是其返回函數實現的,典型的柯里化思想。

相關文章
相關標籤/搜索