摘要:函數(shù)被轉(zhuǎn)化之后得到柯里化函數(shù),能夠處理的所有剩余參數(shù)。因此柯里化也被稱為部分求值。那么函數(shù)的柯里化函數(shù)則可以如下因此下面的運算方式是等價的。而這里對于函數(shù)參數(shù)的自由處理,正是柯里化的核心所在。額外知識補充無限參數(shù)的柯里化。
柯里化是函數(shù)的一個比較高級的應(yīng)用,想要理解它并不簡單。因此我一直在思考應(yīng)該如何更加表達才能讓大家理解起來更加容易。
以下是新版本講解。高階函數(shù)章節(jié)由于一些原因并未公開,大家可以自行搜索學(xué)習(xí)
通過上一個章節(jié)的學(xué)習(xí)我們知道,接收函數(shù)作為參數(shù)的函數(shù),都可以叫做高階函數(shù)。我們常常利用高階函數(shù)來封裝一些公共的邏輯。
這一章我們要學(xué)習(xí)的柯里化,其實就是高階函數(shù)的一種特殊用法。
柯里化是指這樣一個函數(shù)(假設(shè)叫做createCurry),他接收函數(shù)A作為參數(shù),運行后能夠返回一個新的函數(shù)。并且這個新的函數(shù)能夠處理函數(shù)A的剩余參數(shù)。
這樣的定義可能不太好理解,我們可以通過下面的例子配合理解。
假如有一個接收三個參數(shù)的函數(shù)A。
function A(a, b, c) { // do something }
又假如我們有一個已經(jīng)封裝好了的柯里化通用函數(shù)createCurry。他接收bar作為參數(shù),能夠?qū)轉(zhuǎn)化為柯里化函數(shù),返回結(jié)果就是這個被轉(zhuǎn)化之后的函數(shù)。
var _A = createCurry(A);
那么_A作為createCurry運行的返回函數(shù),他能夠處理A的剩余參數(shù)。因此下面的運行結(jié)果都是等價的。
_A(1, 2, 3); _A(1, 2)(3); _A(1)(2, 3); _A(1)(2)(3); A(1, 2, 3);
函數(shù)A被createCurry轉(zhuǎn)化之后得到柯里化函數(shù)_A,_A能夠處理A的所有剩余參數(shù)。因此柯里化也被稱為部分求值。
在簡單的場景下,我們可以不用借助柯里化通用式來轉(zhuǎn)化得到柯里化函數(shù),我們可以憑借眼力自己封裝。
例如有一個簡單的加法函數(shù),他能夠?qū)⒆陨淼娜齻€參數(shù)加起來并返回計算結(jié)果。
function add(a, b, c) { return a + b + c; }
那么add函數(shù)的柯里化函數(shù)_add則可以如下:
function _add(a) { return function(b) { return function(c) { return a + b + c; } } }
因此下面的運算方式是等價的。
add(1, 2, 3); _add(1)(2)(3);
當(dāng)然,柯里化通用式具備更加強大的能力,我們靠眼力自己封裝的柯里化函數(shù)則自由度偏低。因此我們?nèi)匀恍枰雷约喝绾稳シ庋b這樣一個柯里化的通用式。
首先通過_add可以看出,柯里化函數(shù)的運行過程其實是一個參數(shù)的收集過程,我們將每一次傳入的參數(shù)收集起來,并在最里層里面處理。因此我們在實現(xiàn)createCurry時,可以借助這個思路來進行封裝。
封裝如下:
// 簡單實現(xiàn),參數(shù)只能從右到左傳遞 function createCurry(func, args) { var arity = func.length; var args = args || []; return function() { var _args = [].slice.call(arguments); [].push.apply(_args, args); // 如果參數(shù)個數(shù)小于最初的func.length,則遞歸調(diào)用,繼續(xù)收集參數(shù) if (_args.length < arity) { return createCurry.call(this, func, _args); } // 參數(shù)收集完畢,則執(zhí)行func return func.apply(this, _args); } }
盡管我已經(jīng)做了足夠詳細的注解,但是我想理解起來也并不是那么容易,因此建議大家用點耐心多閱讀幾遍。這個createCurry函數(shù)的封裝借助閉包與遞歸,實現(xiàn)了一個參數(shù)收集,并在收集完畢之后執(zhí)行所有參數(shù)的一個過程。
因此聰明的讀者可能已經(jīng)發(fā)現(xiàn),把函數(shù)經(jīng)過createCurry轉(zhuǎn)化為一個柯里化函數(shù),最后執(zhí)行的結(jié)果,不是正好相當(dāng)于執(zhí)行函數(shù)自身嗎?柯里化是不是把簡單的問題復(fù)雜化了?
如果你能夠提出這樣的問題,那么說明你確實已經(jīng)對柯里化有了一定的了解。柯里化確實是把簡答的問題復(fù)雜化了,但是復(fù)雜化的同時,我們在使用函數(shù)時擁有了更加多的自由度。而這里對于函數(shù)參數(shù)的自由處理,正是柯里化的核心所在。
我們來舉一個非常常見的例子。
如果我們想要驗證一串?dāng)?shù)字是否是正確的手機號,那么按照普通的思路來做,大家可能是這樣封裝,如下:
function checkPhone(phoneNumber) { return /^1[34578]d{9}$/.test(phoneNumber); }
而如果我們想要驗證是否是郵箱呢?這么封裝:
function checkEmail(email) { return /^(w)+(.w+)*@(w)+((.w+)+)$/.test(email); }
我們還可能會遇到驗證身份證號,驗證密碼等各種驗證信息,因此在實踐中,為了統(tǒng)一邏輯,,我們就會封裝一個更為通用的函數(shù),將用于驗證的正則與將要被驗證的字符串作為參數(shù)傳入。
function check(targetString, reg) { return reg.test(targetString); }
但是這樣封裝之后,在使用時又會稍微麻煩一點,因為會總是輸入一串正則,這樣就導(dǎo)致了使用時的效率低下。
check(/^1[34578]d{9}$/, "14900000088"); check(/^(w)+(.w+)*@(w)+((.w+)+)$/, "test@163.com");
那么這個時候,我們就可以借助柯里化,在check的基礎(chǔ)上再做一層封裝,以簡化使用。
var _check = createCurry(check); var checkPhone = _check(/^1[34578]d{9}$/); var checkEmail = _check(/^(w)+(.w+)*@(w)+((.w+)+)$/);
最后在使用的時候就會變得更加直觀與簡潔了。
checkPhone("183888888"); checkEmail("xxxxx@test.com");
經(jīng)過這個過程我們發(fā)現(xiàn),柯里化能夠應(yīng)對更加復(fù)雜的邏輯封裝。當(dāng)情況變得多變,柯里化依然能夠應(yīng)付自如。
雖然柯里化確實在一定程度上將問題復(fù)雜化了,也讓代碼更加不容易理解,但是柯里化在面對復(fù)雜情況下的靈活性卻讓我們不得不愛。
當(dāng)然這個案例本身情況還算簡單,所以還不能夠特別明顯的凸顯柯里化的優(yōu)勢,我們的主要目的在于借助這個案例幫助大家了解柯里化在實踐中的用途。
我們繼續(xù)來思考一個例子。這個例子與map有關(guān)。在高階函數(shù)的章節(jié)中,我們分析了封裝map方法的思考過程。由于我們沒有辦法確認(rèn)一個數(shù)組在遍歷時會執(zhí)行什么操作,因此我們只能將調(diào)用for循環(huán)的這個統(tǒng)一邏輯封裝起來,而具體的操作則通過參數(shù)傳入的形式讓使用者自定義。這就是map函數(shù)。
但是,這是針對了所有的情況我們才會這樣想。
實踐中我們常常會發(fā)現(xiàn),在我們的某個項目中,針對于某一個數(shù)組的操作其實是固定的,也就是說,同樣的操作,可能會在項目的不同地方調(diào)用很多次。
于是,這個時候,我們就可以在map函數(shù)的基礎(chǔ)上,進行二次封裝,以簡化我們在項目中的使用。假如這個在我們項目中會調(diào)用多次的操作是將數(shù)組的每一項都轉(zhuǎn)化為百分比 1 --> 100%。
普通思維下我們可以這樣來封裝。
function getNewArray(array) { return array.map(function(item) { return item * 100 + "%" }) } getNewArray([1, 2, 3, 0.12]); // ["100%", "200%", "300%", "12%"];
而如果借助柯里化來二次封裝這樣的邏輯,則會如下實現(xiàn):
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%"]
如果我們的項目中的固定操作是希望對數(shù)組進行一個過濾,找出數(shù)組中的所有Number類型的數(shù)據(jù)。借助柯里化思維我們可以這樣做。
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] // 當(dāng)我們繼續(xù)封裝另外的過濾操作時就會變得非常簡單 // 找出數(shù)字為20的子項 var find20 = _find(function(item, i) { if (typeof item === 20) { return i; } }) find20([1, 2, 3, 30, 20, 100]); // 4 // 找出數(shù)組中大于100的所有數(shù)據(jù) var findGreater100 = _find(function(item) { if (item > 100) { return item; } }) findGreater100([1, 2, 101, 300, 2, 122]); // [101, 300, 122]
我采用了與check例子不一樣的思維方向來想大家展示我們在使用柯里化時的想法。目的是想告訴大家,柯里化能夠幫助我們應(yīng)對更多更復(fù)雜的場景。
當(dāng)然不得不承認(rèn),這些例子都太簡單了,簡單到如果使用柯里化的思維來處理他們顯得有一點多此一舉,而且變得難以理解。因此我想讀者朋友們也很難從這些例子中感受到柯里化的魅力。不過沒關(guān)系,如果我們能夠通過這些例子掌握到柯里化的思維,那就是最好的結(jié)果了。在未來你的實踐中,如果你發(fā)現(xiàn)用普通的思維封裝一些邏輯慢慢變得困難,不妨想一想在這里學(xué)到的柯里化思維,應(yīng)用起來,柯里化足夠強大的自由度一定能給你一個驚喜。
當(dāng)然也并不建議在任何情況下以炫技為目的的去使用柯里化,在柯里化的實現(xiàn)中,我們知道柯里化雖然具有了更多的自由度,但同時柯里化通用式里調(diào)用了arguments對象,使用了遞歸與閉包,因此柯里化的自由度是以犧牲了一定的性能為代價換來的。只有在情況變得復(fù)雜時,才是柯里化大顯身手的時候。
無限參數(shù)的柯里化。
該部分內(nèi)容可忽略
在前端面試中,你可能會遇到這樣一個涉及到柯里化的題目。
// 實現(xiàn)一個add方法,使計算結(jié)果能夠滿足如下預(yù)期: add(1)(2)(3) = 6; add(1, 2, 3)(4) = 10; add(1)(2)(3)(4)(5) = 15;
這個題目的目的是想讓add執(zhí)行之后返回一個函數(shù)能夠繼續(xù)執(zhí)行,最終運算的結(jié)果是所有出現(xiàn)過的參數(shù)之和。而這個題目的難點則在于參數(shù)的不固定。我們不知道函數(shù)會執(zhí)行幾次。因此我們不能使用上面我們封裝的createCurry的通用公式來轉(zhuǎn)換一個柯里化函數(shù)。只能自己封裝,那么怎么辦呢?在此之前,補充2個非常重要的知識點。
一個是ES6函數(shù)的不定參數(shù)。假如我們有一個數(shù)組,希望把這個數(shù)組中所有的子項展開傳遞給一個函數(shù)作為參數(shù)。那么我們應(yīng)該怎么做?
// 大家可以思考一下,如果將args數(shù)組的子項展開作為add的參數(shù)傳入 function add(a, b, c, d) { return a + b + c + d; } var args = [1, 3, 100, 1];
在ES5中,我們可以借助之前學(xué)過的apply來達到我們的目的。
add.apply(null, args); // 105
而在ES6中,提供了一種新的語法來解決這個問題,那就是不定參。寫法如下:
add(...args); // 105
這兩種寫法是等效的。OK,先記在這里。在接下的實現(xiàn)中,我們會用到不定參數(shù)的特性。
第二個要補充的知識點是函數(shù)的隱式轉(zhuǎn)換。當(dāng)我們直接將函數(shù)參與其他的計算時,函數(shù)會默認(rèn)調(diào)用toString方法,直接將函數(shù)體轉(zhuǎn)換為字符串參與計算。
function fn() { return 20 } console.log(fn + 10); // 輸出結(jié)果 function fn() { return 20 }10
但是我們可以重寫函數(shù)的toString方法,讓函數(shù)參與計算時,輸出我們想要的結(jié)果。
function fn() { return 20; } fn.toString = function() { return 30 } console.log(fn + 10); // 40
除此之外,當(dāng)我們重寫函數(shù)的valueOf方法也能夠改變函數(shù)的隱式轉(zhuǎn)換結(jié)果。
function fn() { return 20; } fn.valueOf = function() { return 60 } console.log(fn + 10); // 70
當(dāng)我們同時重寫函數(shù)的toString方法與valueOf方法時,最終的結(jié)果會取valueOf方法的返回結(jié)果。
function fn() { return 20; } fn.valueOf = function() { return 50 } fn.toString = function() { return 30 } console.log(fn + 10); // 60
補充了這兩個知識點之后,我們可以來嘗試完成之前的題目了。add方法的實現(xiàn)仍然會是一個參數(shù)的收集過程。當(dāng)add函數(shù)執(zhí)行到最后時,仍然返回的是一個函數(shù),但是我們可以通過定義toString/valueOf的方式,讓這個函數(shù)可以直接參與計算,并且轉(zhuǎn)換的結(jié)果是我們想要的。而且它本身也仍然可以繼續(xù)執(zhí)行接收新的參數(shù)。實現(xiàn)方式如下。
function add() { // 第一次執(zhí)行時,定義一個數(shù)組專門用來存儲所有的參數(shù) var _args = [].slice.call(arguments); // 在內(nèi)部聲明一個函數(shù),利用閉包的特性保存_args并收集所有的參數(shù)值 var adder = function () { var _adder = function() { // [].push.apply(_args, [].slice.call(arguments)); _args.push(...arguments); return _adder; }; // 利用隱式轉(zhuǎn)換的特性,當(dāng)最后執(zhí)行時隱式轉(zhuǎn)換,并計算最終的值返回 _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 // 可以利用隱式轉(zhuǎn)換的特性參與計算 console.log(a + 10); // 20 console.log(b + 20); // 30 console.log(c + 30); // 40 console.log(d + 40); // 50 // 也可以繼續(xù)傳入?yún)?shù),得到的結(jié)果再次利用隱式轉(zhuǎn)換參與計算 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方法,就是下面這個函數(shù)的柯里化函數(shù),只不過我們并沒有使用通用式來轉(zhuǎn)化,而是自己封裝 function add(...args) { return args.reduce((a, b) => a + b); }
以下為老版本講解,請勿閱讀學(xué)習(xí),因為部分思維并不完全正確。
JavaScript作為一種弱類型語言,它的隱式轉(zhuǎn)換是非常靈活有趣的。當(dāng)我們沒有深入了解隱式轉(zhuǎn)換的時候可能會對一些運算的結(jié)果會感動困惑,比如4 + true = 5。當(dāng)然,如果對隱式轉(zhuǎn)換了解足夠深刻,肯定是能夠很大程度上提高對js的使用能力。只是我沒有打算將所有的隱式轉(zhuǎn)換規(guī)則分享給大家,這里暫時只分享一下,函數(shù)在隱式轉(zhuǎn)換中的一些規(guī)則。
來一個簡單的思考題。
function fn() { return 20; } console.log(fn + 10); // 輸出結(jié)果是多少?
稍微修改一下,再想想輸出結(jié)果會是什么?
function fn() { return 20; } fn.toString = function() { return 10; } console.log(fn + 10); // 輸出結(jié)果是多少?
還可以繼續(xù)修改一下。
function fn() { return 20; } fn.toString = function() { return 10; } fn.valueOf = function() { return 5; } console.log(fn + 10); // 輸出結(jié)果是多少?
// 輸出結(jié)果分別為 function fn() { return 20; }10 20 15
當(dāng)使用console.log,或者進行運算時,隱式轉(zhuǎn)換就可能會發(fā)生。從上面三個例子中我們可以得出一些關(guān)于函數(shù)隱式轉(zhuǎn)換的結(jié)論。
當(dāng)我們沒有重新定義toString與valueOf時,函數(shù)的隱式轉(zhuǎn)換會調(diào)用默認(rèn)的toString方法,它會將函數(shù)的定義內(nèi)容作為字符串返回。而當(dāng)我們主動定義了toString/vauleOf方法時,那么隱式轉(zhuǎn)換的返回結(jié)果則由我們自己控制了。其中valueOf會比toString后執(zhí)行
因此上面例子的結(jié)論就很容易理解了。建議大家動手嘗試一下。
map(): 對數(shù)組中的每一項運行給定函數(shù),返回每次函數(shù)調(diào)用的結(jié)果組成的數(shù)組。
通俗來說,就是遍歷數(shù)組的每一項元素,并且在map的第一個參數(shù)(回調(diào)函數(shù))中進行運算處理后返回計算結(jié)果。返回一個由所有計算結(jié)果組成的新數(shù)組。
// 回調(diào)函數(shù)中有三個參數(shù) // 第一個參數(shù)表示newArr的每一項,第二個參數(shù)表示該項在數(shù)組中的索引值 // 第三個表示數(shù)組本身 // 除此之外,回調(diào)函數(shù)中的this,當(dāng)map不存在第二參數(shù)時,this指向丟失,當(dāng)存在第二個參數(shù)時,指向改參數(shù)所設(shè)定的對象 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方法的細節(jié)。現(xiàn)在要面臨一個難題,就是如何封裝map。
可以先想想for循環(huán)。我們可以使用for循環(huán)來實現(xiàn)一個map,但是在封裝的時候,我們會考慮一些問題。我們在使用for循環(huán)的時候,一個循環(huán)過程確實很好封裝,但是我們在for循環(huán)里面要對每一項做的事情卻很難用一個固定的東西去把它封裝起來。因為每一個場景,for循環(huán)里對數(shù)據(jù)的處理肯定都是不一樣的。
于是大家就想了一個很好的辦法,將這些不一樣的操作多帶帶用一個函數(shù)來處理,讓這個函數(shù)成為map方法的第一個參數(shù),具體這個回調(diào)函數(shù)中會是什么樣的操作,則由我們自己在使用時決定。因此,根據(jù)這個思路的封裝實現(xiàn)如下。
Array.prototype._map = function(fn, context) { var temp = []; if(typeof fn == "function") { var k = 0; var len = this.length; // 封裝for循環(huán)過程 for(; k < len; k++) { // 將每一項的運算操作丟進fn里,利用call方法指定fn的this指向與具體參數(shù) temp.push(fn.call(context, this[k], k, this)) } } else { console.error("TypeError: "+ fn +" is not a function."); } // 返回每一項運算結(jié)果組成的新數(shù)組 return temp; } var newArr = [1, 2, 3, 4]._map(function(item) { return item + 1; }) // [2, 3, 4, 5]
在上面的封裝中,我首先定義了一個空的temp數(shù)組,該數(shù)組用來存儲最終的返回結(jié)果。在for循環(huán)中,每循環(huán)一次,就執(zhí)行一次參數(shù)fn函數(shù),fn的參數(shù)則使用call方法傳入。
在理解了map的封裝過程之后,我們就能夠明白為什么我們在使用map時,總是期望能夠在第一個回調(diào)函數(shù)中有一個返回值了。在eslint的規(guī)則中,如果我們在使用map時沒有設(shè)置一個返回值,就會被判定為錯誤。
ok,明白了函數(shù)的隱式轉(zhuǎn)換規(guī)則與call/apply在這種場景的使用方式,我們就可以嘗試通過簡單的例子來了解一下柯里化了。
在前端面試中有一個關(guān)于柯里化的面試題,流傳甚廣。
實現(xiàn)一個add方法,使計算結(jié)果能夠滿足如下預(yù)期:
add(1)(2)(3) = 6
add(1, 2, 3)(4) = 10
add(1)(2)(3)(4)(5) = 15
很明顯,計算結(jié)果正是所有參數(shù)的和,add方法每運行一次,肯定返回了一個同樣的函數(shù),繼續(xù)計算剩下的參數(shù)。
我們可以從最簡單的例子一步一步尋找解決方案。
當(dāng)我們只調(diào)用兩次時,可以這樣封裝。
function add(a) { return function(b) { return a + b; } } console.log(add(1)(2)); // 3
如果只調(diào)用三次:
function add(a) { return function(b) { return function (c) { return a + b + c; } } } console.log(add(1)(2)(3)); // 6
上面的封裝看上去跟我們想要的結(jié)果有點類似,但是參數(shù)的使用被限制得很死,因此并不是我們想要的最終結(jié)果,我們需要通用的封裝。應(yīng)該怎么辦?總結(jié)一下上面2個例子,其實我們是利用閉包的特性,將所有的參數(shù),集中到最后返回的函數(shù)里進行計算并返回結(jié)果。因此我們在封裝時,主要的目的,就是將參數(shù)集中起來計算。
來看看具體實現(xiàn)。
function add() { // 第一次執(zhí)行時,定義一個數(shù)組專門用來存儲所有的參數(shù) var _args = [].slice.call(arguments); // 在內(nèi)部聲明一個函數(shù),利用閉包的特性保存_args并收集所有的參數(shù)值 var adder = function () { var _adder = function() { [].push.apply(_args, [].slice.call(arguments)); return _adder; }; // 利用隱式轉(zhuǎn)換的特性,當(dāng)最后執(zhí)行時隱式轉(zhuǎn)換,并計算最終的值返回 _adder.toString = function () { return _args.reduce(function (a, b) { return a + b; }); } return _adder; } return adder.apply(null, [].slice.call(arguments)); } // 輸出結(jié)果,可自由組合的參數(shù) 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
上面的實現(xiàn),利用閉包的特性,主要目的是想通過一些巧妙的方法將所有的參數(shù)收集在一個數(shù)組里,并在最終隱式轉(zhuǎn)換時將數(shù)組里的所有項加起來。因此我們在調(diào)用add方法的時候,參數(shù)就顯得非常靈活。當(dāng)然,也就很輕松的滿足了我們的需求。
那么讀懂了上面的demo,然后我們再來看看柯里化的定義,相信大家就會更加容易理解了。
柯里化(英語:Currying),又稱為部分求值,是把接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù)(最初函數(shù)的第一個參數(shù))的函數(shù),并且返回一個新的函數(shù)的技術(shù),新函數(shù)接受余下參數(shù)并返回運算結(jié)果。
接收單一參數(shù),因為要攜帶不少信息,因此常常以回調(diào)函數(shù)的理由來解決。
將部分參數(shù)通過回調(diào)函數(shù)等方式傳入函數(shù)中
返回一個新函數(shù),用于處理所有的想要傳入的參數(shù)
在上面的例子中,我們可以將add(1, 2, 3, 4)轉(zhuǎn)換為add(1)(2)(3)(4)。這就是部分求值。每次傳入的參數(shù)都只是我們想要傳入的所有參數(shù)中的一部分。當(dāng)然實際應(yīng)用中,并不會常常這么復(fù)雜的去處理參數(shù),很多時候也僅僅只是分成兩部分而已。
咱們再來一起思考一個與柯里化相關(guān)的問題。
假如有一個計算要求,需要我們將數(shù)組里面的每一項用我們自己想要的字符給連起來。我們應(yīng)該怎么做?想到使用join方法,就很簡單。
var arr = [1, 2, 3, 4, 5]; // 實際開發(fā)中并不建議直接給Array擴展新的方法 // 只是用這種方式演示能夠更加清晰一點 Array.prototype.merge = function(chars) { return this.join(chars); } var string = arr.merge("-") console.log(string); // 1-2-3-4-5
增加難度,將每一項加一個數(shù)后再連起來。那么這里就需要map來幫助我們對每一項進行特殊的運算處理,生成新的數(shù)組然后用字符連接起來了。實現(xiàn)如下:
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
但是如果我們又想要讓數(shù)組每一項都減去一個數(shù)之后再連起來呢?當(dāng)然和上面的加法操作一樣的實現(xiàn)。
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
機智的小伙伴肯定發(fā)現(xiàn)困惑所在了。我們期望封裝一個函數(shù),能同時處理不同的運算過程,但是我們并不能使用一個固定的套路將對每一項的操作都封裝起來。于是問題就變成了和封裝map的時候所面臨的問題一樣了。我們可以借助柯里化來搞定。
與map封裝同樣的道理,既然我們事先并不確定我們將要對每一項數(shù)據(jù)進行怎么樣的處理,我只是知道我們需要將他們處理之后然后用字符連起來,所以不妨將處理內(nèi)容保存在一個函數(shù)里。而僅僅固定封裝連起來的這一部分需求。
于是我們就有了以下的封裝。
// 封裝很簡單,一句話搞定 Array.prototype.merge = function(fn, chars) { return this.map(fn).join(chars); } var arr = [1, 2, 3, 4]; // 難點在于,在實際使用的時候,操作怎么來定義,利用閉包保存于傳遞num參數(shù) 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), "-"); // 也可以直接使用回調(diào)函數(shù),每一項乘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
大家能從上面的例子,發(fā)現(xiàn)柯里化的特征嗎?
通用的柯里化寫法其實比我們上邊封裝的add方法要簡單許多。
var currying = function(fn) { var args = [].slice.call(arguments, 1); return function() { // 主要還是收集所有需要的參數(shù)到一個數(shù)組中,便于統(tǒng)一計算 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的靈活運用,實現(xiàn)了bind的功能。
在前面的幾個例子中,我們可以總結(jié)一下柯里化的特點:
接收單一參數(shù),將更多的參數(shù)通過回調(diào)函數(shù)來搞定?
返回一個新函數(shù),用于處理所有的想要傳入的參數(shù);
需要利用call/apply與arguments對象收集參數(shù);
返回的這個函數(shù)正是用來處理收集起來的參數(shù)。
希望大家讀完之后都能夠大概明白柯里化的概念,如果想要熟練使用它,就需要我們掌握更多的實際經(jīng)驗才行。
前端基礎(chǔ)進階系列目錄
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/90539.html
摘要:在前端基礎(chǔ)進階八深入詳解函數(shù)的柯里化一文中,我有分享柯里化相關(guān)的知識。雖然說高階組件與柯里化都屬于比較難以理解的知識點,但是他們組合在一起使用時并沒有新增更多的難點。 可能看過我以前文章的同學(xué)應(yīng)該會猜得到當(dāng)我用New的方法來舉例學(xué)習(xí)高階組件時,接下來要分享的就是柯里化了。高階組件與函數(shù)柯里化的運用是非常能夠提高代碼逼格的技巧,如果你有剩余的精力,完全可以花點時間學(xué)習(xí)一下。 在前端基礎(chǔ)進...
摘要:不過其實簡書文章評論里有很多大家的問題以及解答,對于進一步理解文中知識幫助很大的,算是有點可惜吧。不過也希望能夠?qū)φ趯W(xué)習(xí)前端的你有一些小幫助。如果在閱讀中發(fā)現(xiàn)了一些錯誤,請在評論里告訴我,我會及時更改。 前端基礎(chǔ)進階(一):內(nèi)存空間詳細圖解 前端基礎(chǔ)進階(二):執(zhí)行上下文詳細圖解 前端基礎(chǔ)進階(三):變量對象詳解 前端基礎(chǔ)進階(四):詳細圖解作用域鏈與閉包 前端基礎(chǔ)進階(五):全方位...
摘要:函數(shù)柯里化是把支持多個參數(shù)的函數(shù)變成接收單一參數(shù)的函數(shù),并返回一個函數(shù)能接收處理剩余參數(shù),而反柯里化就是把參數(shù)全部釋放出來。但在一些復(fù)雜的業(yè)務(wù)邏輯封裝中,函數(shù)柯里化能夠為我們提供更好的應(yīng)對方案,讓我們的函數(shù)更具自由度和靈活性。 showImg(https://segmentfault.com/img/bVburN1?w=800&h=600); 柯里化(Curring, 以邏輯學(xué)家Has...
摘要:最直接的方式當(dāng)然是遍歷數(shù)組并累加得到結(jié)果,也可以使用數(shù)組的方法實現(xiàn),如下結(jié)合第四步,替換中的返回值即可將其進行簡化,得到最終結(jié)果當(dāng)然,采用這種實現(xiàn)方式,對于形如的調(diào)用方式也是沒有問題的。 談?wù)?JavaScript 中形如 add(1)(2)(3)(4) = 10 這種累加器方法的實現(xiàn)過程和思路 第一步:實現(xiàn)級聯(lián) 若是想要實現(xiàn) fn()() 這種調(diào)用方式的函數(shù),則在 fn 函數(shù)體內(nèi)一定...
摘要:函數(shù)式編程,一看這個詞,簡直就是學(xué)院派的典范。所以這期周刊,我們就重點引入的函數(shù)式編程,淺入淺出,一窺函數(shù)式編程的思想,可能讓你對編程語言的理解更加融會貫通一些。但從根本上來說,函數(shù)式編程就是關(guān)于如使用通用的可復(fù)用函數(shù)進行組合編程。 showImg(https://segmentfault.com/img/bVGQuc); 函數(shù)式編程(Functional Programming),一...
閱讀 2645·2023-04-26 02:17
閱讀 1619·2021-11-24 09:39
閱讀 1079·2021-11-18 13:13
閱讀 2649·2021-09-02 15:11
閱讀 2781·2019-08-30 15:48
閱讀 3412·2019-08-30 14:00
閱讀 2443·2019-08-29 13:43
閱讀 666·2019-08-29 13:07