柯里化是函數的一個比較高級的應用,想要理解它並不簡單。所以我一直在思考應該如何更加表達才能讓你們理解起來更加容易。javascript
如下是新版本講解。高階函數章節因爲一些緣由並未公開,你們能夠自行搜索學習
經過上一個章節的學習咱們知道,接收函數做爲參數的函數,均可以叫作高階函數。咱們經常利用高階函數來封裝一些公共的邏輯。前端
這一章咱們要學習的柯里化,其實就是高階函數的一種特殊用法。java
柯里化是指這樣一個函數(假設叫作createCurry),他接收函數A做爲參數,運行後可以返回一個新的函數。而且這個新的函數可以處理函數A的剩餘參數。面試
這樣的定義可能不太好理解,咱們能夠經過下面的例子配合理解。segmentfault
假若有一個接收三個參數的函數A。數組
function A(a, b, c) { // do something }
又假如咱們有一個已經封裝好了的柯里化通用函數createCurry。他接收bar做爲參數,可以將A轉化爲柯里化函數,返回結果就是這個被轉化以後的函數。閉包
var _A = createCurry(A);
那麼_A做爲createCurry運行的返回函數,他可以處理A的剩餘參數。所以下面的運行結果都是等價的。app
_A(1, 2, 3); _A(1, 2)(3); _A(1)(2, 3); _A(1)(2)(3); A(1, 2, 3);
函數A被createCurry轉化以後獲得柯里化函數_A,_A可以處理A的全部剩餘參數。所以柯里化也被稱爲部分求值。函數
在簡單的場景下,咱們能夠不用藉助柯里化通用式來轉化獲得柯里化函數,咱們能夠憑藉眼力本身封裝。性能
例若有一個簡單的加法函數,他可以將自身的三個參數加起來並返回計算結果。
function add(a, b, c) { return a + b + c; }
那麼add函數的柯里化函數_add則能夠以下:
function _add(a) { return function(b) { return function(c) { return a + b + c; } } }
所以下面的運算方式是等價的。
add(1, 2, 3); _add(1)(2)(3);
固然,柯里化通用式具有更增強大的能力,咱們靠眼力本身封裝的柯里化函數則自由度偏低。所以咱們仍然須要知道本身如何去封裝這樣一個柯里化的通用式。
首先經過_add能夠看出,柯里化函數的運行過程實際上是一個參數的收集過程,咱們將每一次傳入的參數收集起來,並在最裏層裏面處理。所以咱們在實現createCurry時,能夠藉助這個思路來進行封裝。
封裝以下:
// 簡單實現,參數只能從右到左傳遞 function createCurry(func, args) { var arity = func.length; var args = args || []; return function() { var _args = [].slice.call(arguments); [].push.apply(_args, args); // 若是參數個數小於最初的func.length,則遞歸調用,繼續收集參數 if (_args.length < arity) { return createCurry.call(this, func, _args); } // 參數收集完畢,則執行func return func.apply(this, _args); } }
儘管我已經作了足夠詳細的註解,可是我想理解起來也並非那麼容易,所以建議你們用點耐心多閱讀幾遍。這個createCurry函數的封裝藉助閉包與遞歸,實現了一個參數收集,並在收集完畢以後執行全部參數的一個過程。
所以聰明的讀者可能已經發現,把函數通過createCurry轉化爲一個柯里化函數,最後執行的結果,不是正好至關於執行函數自身嗎?柯里化是否是把簡單的問題複雜化了?
若是你可以提出這樣的問題,那麼說明你確實已經對柯里化有了必定的瞭解。柯里化確實是把簡答的問題複雜化了,可是複雜化的同時,咱們在使用函數時擁有了更加多的自由度。而這裏對於函數參數的自由處理,正是柯里化的核心所在。
咱們來舉一個很是常見的例子。
若是咱們想要驗證一串數字是不是正確的手機號,那麼按照普通的思路來作,你們多是這樣封裝,以下:
function checkPhone(phoneNumber) { return /^1[34578]\d{9}$/.test(phoneNumber); }
而若是咱們想要驗證是不是郵箱呢?這麼封裝:
function checkEmail(email) { return /^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/.test(email); }
咱們還可能會遇到驗證身份證號,驗證密碼等各類驗證信息,所以在實踐中,爲了統一邏輯,,咱們就會封裝一個更爲通用的函數,將用於驗證的正則與將要被驗證的字符串做爲參數傳入。
function check(targetString, reg) { return reg.test(targetString); }
可是這樣封裝以後,在使用時又會稍微麻煩一點,由於會老是輸入一串正則,這樣就致使了使用時的效率低下。
check(/^1[34578]\d{9}$/, '14900000088'); check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com');
那麼這個時候,咱們就能夠藉助柯里化,在check的基礎上再作一層封裝,以簡化使用。
var _check = createCurry(check); var checkPhone = _check(/^1[34578]\d{9}$/); var checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
最後在使用的時候就會變得更加直觀與簡潔了。
checkPhone('183888888'); checkEmail('xxxxx@test.com');
通過這個過程咱們發現,柯里化可以應對更加複雜的邏輯封裝。當狀況變得多變,柯里化依然可以應付自如。
雖然柯里化確實在必定程度上將問題複雜化了,也讓代碼更加不容易理解,可是柯里化在面對複雜狀況下的靈活性卻讓咱們不得不愛。
固然這個案例自己狀況還算簡單,因此還不可以特別明顯的凸顯柯里化的優點,咱們的主要目的在於藉助這個案例幫助你們瞭解柯里化在實踐中的用途。
咱們繼續來思考一個例子。這個例子與map有關。在高階函數的章節中,咱們分析了封裝map方法的思考過程。因爲咱們沒有辦法確認一個數組在遍歷時會執行什麼操做,所以咱們只能將調用for循環的這個統一邏輯封裝起來,而具體的操做則經過參數傳入的形式讓使用者自定義。這就是map函數。
可是,這是針對了全部的狀況咱們纔會這樣想。
實踐中咱們經常會發現,在咱們的某個項目中,針對於某一個數組的操做實際上是固定的,也就是說,一樣的操做,可能會在項目的不一樣地方調用不少次。
因而,這個時候,咱們就能夠在map函數的基礎上,進行二次封裝,以簡化咱們在項目中的使用。假如這個在咱們項目中會調用屢次的操做是將數組的每一項都轉化爲百分比 1 --> 100%。
普通思惟下咱們能夠這樣來封裝。
function getNewArray(array) { return array.map(function(item) { return item * 100 + '%' }) } getNewArray([1, 2, 3, 0.12]); // ['100%', '200%', '300%', '12%'];
而若是藉助柯里化來二次封裝這樣的邏輯,則會以下實現:
function _map(func, array) { return array.map(func); } var _getNewArray = createCurry(_map); var getNewArray = _getNewArray(function(item) { return item * 100 + '%' }) getNewArray([1, 2, 3, 0.12]); // ['100%', '200%', '300%', '12%']; getNewArray([0.01, 1]); // ['1%', '100%']
若是咱們的項目中的固定操做是但願對數組進行一個過濾,找出數組中的全部Number類型的數據。藉助柯里化思惟咱們能夠這樣作。
function _filter(func, array) { return array.filter(func); } var _find = createCurry(_filter); var findNumber = _find(function(item) { if (typeof item == 'number') { return item; } }) findNumber([1, 2, 3, '2', '3', 4]); // [1, 2, 3, 4] // 當咱們繼續封裝另外的過濾操做時就會變得很是簡單 // 找出數字爲20的子項 var find20 = _find(function(item, i) { if (typeof item === 20) { return i; } }) find20([1, 2, 3, 30, 20, 100]); // 4 // 找出數組中大於100的全部數據 var findGreater100 = _find(function(item) { if (item > 100) { return item; } }) findGreater100([1, 2, 101, 300, 2, 122]); // [101, 300, 122]
我採用了與check例子不同的思惟方向來想你們展現咱們在使用柯里化時的想法。目的是想告訴你們,柯里化可以幫助咱們應對更多更復雜的場景。
固然不得不認可,這些例子都太簡單了,簡單到若是使用柯里化的思惟來處理他們顯得有一點畫蛇添足,並且變得難以理解。所以我想讀者朋友們也很難從這些例子中感覺到柯里化的魅力。不過不要緊,若是咱們可以經過這些例子掌握到柯里化的思惟,那就是最好的結果了。在將來你的實踐中,若是你發現用普通的思惟封裝一些邏輯慢慢變得困難,不妨想想在這裏學到的柯里化思惟,應用起來,柯里化足夠強大的自由度必定能給你一個驚喜。
固然也並不建議在任何狀況下以炫技爲目的的去使用柯里化,在柯里化的實現中,咱們知道柯里化雖然具備了更多的自由度,但同時柯里化通用式裏調用了arguments對象,使用了遞歸與閉包,所以柯里化的自由度是以犧牲了必定的性能爲代價換來的。只有在狀況變得複雜時,纔是柯里化大顯身手的時候。
無限參數的柯里化。
該部份內容可忽略
在前端面試中,你可能會遇到這樣一個涉及到柯里化的題目。
// 實現一個add方法,使計算結果可以知足以下預期: add(1)(2)(3) = 6; add(1, 2, 3)(4) = 10; add(1)(2)(3)(4)(5) = 15;
這個題目的目的是想讓add執行以後返回一個函數可以繼續執行,最終運算的結果是全部出現過的參數之和。而這個題目的難點則在於參數的不固定。咱們不知道函數會執行幾回。所以咱們不能使用上面咱們封裝的createCurry的通用公式來轉換一個柯里化函數。只能本身封裝,那麼怎麼辦呢?在此以前,補充2個很是重要的知識點。
一個是ES6函數的不定參數。假如咱們有一個數組,但願把這個數組中全部的子項展開傳遞給一個函數做爲參數。那麼咱們應該怎麼作?
// 你們能夠思考一下,若是將args數組的子項展開做爲add的參數傳入 function add(a, b, c, d) { return a + b + c + d; } var args = [1, 3, 100, 1];
在ES5中,咱們能夠藉助以前學過的apply來達到咱們的目的。
add.apply(null, args); // 105
而在ES6中,提供了一種新的語法來解決這個問題,那就是不定參。寫法以下:
add(...args); // 105
這兩種寫法是等效的。OK,先記在這裏。在接下的實現中,咱們會用到不定參數的特性。
第二個要補充的知識點是函數的隱式轉換。當咱們直接將函數參與其餘的計算時,函數會默認調用toString方法,直接將函數體轉換爲字符串參與計算。
function fn() { return 20 } console.log(fn + 10); // 輸出結果 function fn() { return 20 }10
可是咱們能夠重寫函數的toString方法,讓函數參與計算時,輸出咱們想要的結果。
function fn() { return 20; } fn.toString = function() { return 30 } console.log(fn + 10); // 40
除此以外,當咱們重寫函數的valueOf方法也可以改變函數的隱式轉換結果。
function fn() { return 20; } fn.valueOf = function() { return 60 } console.log(fn + 10); // 70
當咱們同時重寫函數的toString方法與valueOf方法時,最終的結果會取valueOf方法的返回結果。
function fn() { return 20; } fn.valueOf = function() { return 50 } fn.toString = function() { return 30 } console.log(fn + 10); // 60
補充了這兩個知識點以後,咱們能夠來嘗試完成以前的題目了。add方法的實現仍然會是一個參數的收集過程。當add函數執行到最後時,仍然返回的是一個函數,可是咱們能夠經過定義toString/valueOf的方式,讓這個函數能夠直接參與計算,而且轉換的結果是咱們想要的。並且它自己也仍然能夠繼續執行接收新的參數。實現方式以下。
function add() { // 第一次執行時,定義一個數組專門用來存儲全部的參數 var _args = [].slice.call(arguments); // 在內部聲明一個函數,利用閉包的特性保存_args並收集全部的參數值 var adder = function () { var _adder = function() { // [].push.apply(_args, [].slice.call(arguments)); _args.push(...arguments); return _adder; }; // 利用隱式轉換的特性,當最後執行時隱式轉換,並計算最終的值返回 _adder.toString = function () { return _args.reduce(function (a, b) { return a + b; }); } return _adder; } // return adder.apply(null, _args); return adder(..._args); } var a = add(1)(2)(3)(4); // f 10 var b = add(1, 2, 3, 4); // f 10 var c = add(1, 2)(3, 4); // f 10 var d = add(1, 2, 3)(4); // f 10 // 能夠利用隱式轉換的特性參與計算 console.log(a + 10); // 20 console.log(b + 20); // 30 console.log(c + 30); // 40 console.log(d + 40); // 50 // 也能夠繼續傳入參數,獲得的結果再次利用隱式轉換參與計算 console.log(a(10) + 100); // 120 console.log(b(10) + 100); // 120 console.log(c(10) + 100); // 120 console.log(d(10) + 100); // 120
// 其實上慄中的add方法,就是下面這個函數的柯里化函數,只不過咱們並無使用通用式來轉化,而是本身封裝 function add(...args) { return args.reduce((a, b) => a + b); }
如下爲老版本講解,請勿閱讀學習,由於部分思惟並不徹底正確。
JavaScript做爲一種弱類型語言,它的隱式轉換是很是靈活有趣的。當咱們沒有深刻了解隱式轉換的時候可能會對一些運算的結果會感動困惑,好比4 + true = 5
。固然,若是對隱式轉換了解足夠深入,確定是可以很大程度上提升對js的使用能力。只是我沒有打算將全部的隱式轉換規則分享給你們,這裏暫時只分享一下,函數在隱式轉換中的一些規則。
來一個簡單的思考題。
function fn() { return 20; } console.log(fn + 10); // 輸出結果是多少?
稍微修改一下,再想一想輸出結果會是什麼?
function fn() { return 20; } fn.toString = function() { return 10; } console.log(fn + 10); // 輸出結果是多少?
還能夠繼續修改一下。
function fn() { return 20; } fn.toString = function() { return 10; } fn.valueOf = function() { return 5; } console.log(fn + 10); // 輸出結果是多少?
// 輸出結果分別爲 function fn() { return 20; }10 20 15
當使用console.log,或者進行運算時,隱式轉換就可能會發生。從上面三個例子中咱們能夠得出一些關於函數隱式轉換的結論。
當咱們沒有從新定義toString與valueOf時,函數的隱式轉換會調用默認的toString方法,它會將函數的定義內容做爲字符串返回。而當咱們主動定義了toString/vauleOf方法時,那麼隱式轉換的返回結果則由咱們本身控制了。其中valueOf會比toString後執行
所以上面例子的結論就很容易理解了。建議你們動手嘗試一下。
map(): 對數組中的每一項運行給定函數,返回每次函數調用的結果組成的數組。
通俗來講,就是遍歷數組的每一項元素,而且在map的第一個參數(回調函數)中進行運算處理後返回計算結果。返回一個由全部計算結果組成的新數組。
// 回調函數中有三個參數 // 第一個參數表示newArr的每一項,第二個參數表示該項在數組中的索引值 // 第三個表示數組自己 // 除此以外,回調函數中的this,當map不存在第二參數時,this指向丟失,當存在第二個參數時,指向改參數所設定的對象 var newArr = [1, 2, 3, 4].map(function(item, i, arr) { console.log(item, i, arr, this); // 可運行試試看 return item + 1; // 每一項加1 }, { a: 1 }) console.log(newArr); // [2, 3, 4, 5]
在上面例子的註釋中詳細闡述了map方法的細節。如今要面臨一個難題,就是如何封裝map。
能夠先想一想for循環。咱們可使用for循環來實現一個map,可是在封裝的時候,咱們會考慮一些問題。咱們在使用for循環的時候,一個循環過程確實很好封裝,可是咱們在for循環裏面要對每一項作的事情卻很難用一個固定的東西去把它封裝起來。由於每個場景,for循環裏對數據的處理確定都是不同的。
因而你們就想了一個很好的辦法,將這些不同的操做單獨用一個函數來處理,讓這個函數成爲map方法的第一個參數,具體這個回調函數中會是什麼樣的操做,則由咱們本身在使用時決定。所以,根據這個思路的封裝實現以下。
Array.prototype._map = function(fn, context) { var temp = []; if(typeof fn == 'function') { var k = 0; var len = this.length; // 封裝for循環過程 for(; k < len; k++) { // 將每一項的運算操做丟進fn裏,利用call方法指定fn的this指向與具體參數 temp.push(fn.call(context, this[k], k, this)) } } else { console.error('TypeError: '+ fn +' is not a function.'); } // 返回每一項運算結果組成的新數組 return temp; } var newArr = [1, 2, 3, 4]._map(function(item) { return item + 1; }) // [2, 3, 4, 5]
在上面的封裝中,我首先定義了一個空的temp數組,該數組用來存儲最終的返回結果。在for循環中,每循環一次,就執行一次參數fn函數,fn的參數則使用call方法傳入。
在理解了map的封裝過程以後,咱們就可以明白爲何咱們在使用map時,老是指望可以在第一個回調函數中有一個返回值了。在eslint的規則中,若是咱們在使用map時沒有設置一個返回值,就會被斷定爲錯誤。
ok,明白了函數的隱式轉換規則與call/apply在這種場景的使用方式,咱們就能夠嘗試經過簡單的例子來了解一下柯里化了。
在前端面試中有一個關於柯里化的面試題,流傳甚廣。
實現一個add方法,使計算結果可以知足以下預期:
add(1)(2)(3) = 6
add(1, 2, 3)(4) = 10
add(1)(2)(3)(4)(5) = 15
很明顯,計算結果正是全部參數的和,add方法每運行一次,確定返回了一個一樣的函數,繼續計算剩下的參數。
咱們能夠從最簡單的例子一步一步尋找解決方案。
當咱們只調用兩次時,能夠這樣封裝。
function add(a) { return function(b) { return a + b; } } console.log(add(1)(2)); // 3
若是隻調用三次:
function add(a) { return function(b) { return function (c) { return a + b + c; } } } console.log(add(1)(2)(3)); // 6
上面的封裝看上去跟咱們想要的結果有點相似,可是參數的使用被限制得很死,所以並非咱們想要的最終結果,咱們須要通用的封裝。應該怎麼辦?總結一下上面2個例子,其實咱們是利用閉包的特性,將全部的參數,集中到最後返回的函數裏進行計算並返回結果。所以咱們在封裝時,主要的目的,就是將參數集中起來計算。
來看看具體實現。
function add() { // 第一次執行時,定義一個數組專門用來存儲全部的參數 var _args = [].slice.call(arguments); // 在內部聲明一個函數,利用閉包的特性保存_args並收集全部的參數值 var adder = function () { var _adder = function() { [].push.apply(_args, [].slice.call(arguments)); return _adder; }; // 利用隱式轉換的特性,當最後執行時隱式轉換,並計算最終的值返回 _adder.toString = function () { return _args.reduce(function (a, b) { return a + b; }); } return _adder; } return adder.apply(null, [].slice.call(arguments)); } // 輸出結果,可自由組合的參數 console.log(add(1, 2, 3, 4, 5)); // 15 console.log(add(1, 2, 3, 4)(5)); // 15 console.log(add(1)(2)(3)(4)(5)); // 15
上面的實現,利用閉包的特性,主要目的是想經過一些巧妙的方法將全部的參數收集在一個數組裏,並在最終隱式轉換時將數組裏的全部項加起來。所以咱們在調用add方法的時候,參數就顯得很是靈活。固然,也就很輕鬆的知足了咱們的需求。
那麼讀懂了上面的demo,而後咱們再來看看柯里化的定義,相信你們就會更加容易理解了。
柯里化(英語:Currying),又稱爲部分求值,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回一個新的函數的技術,新函數接受餘下參數並返回運算結果。
在上面的例子中,咱們能夠將add(1, 2, 3, 4)
轉換爲add(1)(2)(3)(4)
。這就是部分求值。每次傳入的參數都只是咱們想要傳入的全部參數中的一部分。固然實際應用中,並不會經常這麼複雜的去處理參數,不少時候也僅僅只是分紅兩部分而已。
我們再來一塊兒思考一個與柯里化相關的問題。
假若有一個計算要求,須要咱們將數組裏面的每一項用咱們本身想要的字符給連起來。咱們應該怎麼作?想到使用join方法,就很簡單。
var arr = [1, 2, 3, 4, 5]; // 實際開發中並不建議直接給Array擴展新的方法 // 只是用這種方式演示可以更加清晰一點 Array.prototype.merge = function(chars) { return this.join(chars); } var string = arr.merge('-') console.log(string); // 1-2-3-4-5
增長難度,將每一項加一個數後再連起來。那麼這裏就須要map來幫助咱們對每一項進行特殊的運算處理,生成新的數組而後用字符鏈接起來了。實現以下:
var arr = [1, 2, 3, 4, 5]; Array.prototype.merge = function(chars, number) { return this.map(function(item) { return item + number; }).join(chars); } var string = arr.merge('-', 1); console.log(string); // 2-3-4-5-6
可是若是咱們又想要讓數組每一項都減去一個數以後再連起來呢?固然和上面的加法操做同樣的實現。
var arr = [1, 2, 3, 4, 5]; Array.prototype.merge = function(chars, number) { return this.map(function(item) { return item - number; }).join(chars); } var string = arr.merge('~', 1); console.log(string); // 0~1~2~3~4
機智的小夥伴確定發現困惑所在了。咱們指望封裝一個函數,能同時處理不一樣的運算過程,可是咱們並不能使用一個固定的套路將對每一項的操做都封裝起來。因而問題就變成了和封裝map的時候所面臨的問題同樣了。咱們能夠藉助柯里化來搞定。
與map封裝一樣的道理,既然咱們事先並不肯定咱們將要對每一項數據進行怎麼樣的處理,我只是知道咱們須要將他們處理以後而後用字符連起來,因此不妨將處理內容保存在一個函數裏。而僅僅固定封裝連起來的這一部分需求。
因而咱們就有了如下的封裝。
// 封裝很簡單,一句話搞定 Array.prototype.merge = function(fn, chars) { return this.map(fn).join(chars); } var arr = [1, 2, 3, 4]; // 難點在於,在實際使用的時候,操做怎麼來定義,利用閉包保存於傳遞num參數 var add = function(num) { return function(item) { return item + num; } } var red = function(num) { return function(item) { return item - num; } } // 每一項加2後合併 var res1 = arr.merge(add(2), '-'); // 每一項減2後合併 var res2 = arr.merge(red(1), '-'); // 也能夠直接使用回調函數,每一項乘2後合併 var res3 = arr.merge((function(num) { return function(item) { return item * num } })(2), '-') console.log(res1); // 3-4-5-6 console.log(res2); // 0-1-2-3 console.log(res3); // 2-4-6-8
你們能從上面的例子,發現柯里化的特徵嗎?
通用的柯里化寫法其實比咱們上邊封裝的add方法要簡單許多。
var currying = function(fn) { var args = [].slice.call(arguments, 1); return function() { // 主要仍是收集全部須要的參數到一個數組中,便於統一計算 var _args = args.concat([].slice.call(arguments)); return fn.apply(null, _args); } } var sum = currying(function() { var args = [].slice.call(arguments); return args.reduce(function(a, b) { return a + b; }) }, 10) console.log(sum(20, 10)); // 40 console.log(sum(10, 5)); // 25
Object.prototype.bind = function(context) { var _this = this; var args = [].slice.call(arguments, 1); return function() { return _this.apply(context, args) } }
這個例子利用call與apply的靈活運用,實現了bind的功能。
在前面的幾個例子中,咱們能夠總結一下柯里化的特色:
但願你們讀完以後都可以大概明白柯里化的概念,若是想要熟練使用它,就須要咱們掌握更多的實際經驗才行。