摘要:原題如下寫一個(gè)方法,當(dāng)使用下面的語法調(diào)用時(shí),能正常工作這道題要考察的,就是對(duì)函數(shù)柯里化的理解。當(dāng)參數(shù)只有一個(gè)的時(shí)候,進(jìn)行柯里化的處理。這其實(shí)就是函數(shù)柯里化的簡(jiǎn)單應(yīng)用。
前言
這是前端面試題系列的第 6 篇,你可能錯(cuò)過了前面的篇章,可以在這里找到:
ES6 中箭頭函數(shù)的用法
this 的原理以及用法
偽類與偽元素的區(qū)別及實(shí)戰(zhàn)
如何實(shí)現(xiàn)一個(gè)圣杯布局?
今日頭條 面試題和思路解析
最近,朋友T 在準(zhǔn)備面試,他為一道編程題所困,向我求助。原題如下:
// 寫一個(gè) sum 方法,當(dāng)使用下面的語法調(diào)用時(shí),能正常工作 console.log(sum(2, 3)); // Outputs 5 console.log(sum(2)(3)); // Outputs 5
這道題要考察的,就是對(duì)函數(shù)柯里化的理解。讓我們先來解析一下題目的要求:
如果傳遞兩個(gè)參數(shù),我們只需將它們相加并返回。
否則,我們假設(shè)它是以sum(2)(3)的形式被調(diào)用的,所以我們返回一個(gè)匿名函數(shù),它將傳遞給sum()(在本例中為2)的參數(shù)和傳遞給匿名函數(shù)的參數(shù)(在本例中為3)。
所以,sum 函數(shù)可以這樣寫:
function sum (x) { if (arguments.length == 2) { return arguments[0] + arguments[1]; } return function(y) { return x + y; } }
arguments 的用法挺靈活的,在這里它則用于分割兩種不同的情況。當(dāng)參數(shù)只有一個(gè)的時(shí)候,進(jìn)行柯里化的處理。
那么,到底什么是函數(shù)的柯里化呢?接下來,我們將從概念出發(fā),探究函數(shù)柯里化的實(shí)現(xiàn)與用途。
什么是柯里化柯里化,是函數(shù)式編程的一個(gè)重要概念。它既能減少代碼冗余,也能增加可讀性。另外,附帶著還能用來裝逼。
先給出柯里化的定義:在數(shù)學(xué)和計(jì)算機(jī)科學(xué)中,柯里化是一種將使用多個(gè)參數(shù)的一個(gè)函數(shù)轉(zhuǎn)換成一系列使用一個(gè)參數(shù)的函數(shù)的技術(shù)。
柯里化的定義,理解起來有點(diǎn)費(fèi)勁。為了更好地理解,先看下面這個(gè)例子:
function sum (a, b, c) { console.log(a + b + c); } sum(1, 2, 3); // 6
毫無疑問,sum 是個(gè)簡(jiǎn)單的累加函數(shù),接受3個(gè)參數(shù),輸出累加的結(jié)果。
假設(shè)有這樣的需求,sum的前2個(gè)參數(shù)保持不變,最后一個(gè)參數(shù)可以隨意。那么就會(huì)想到,在函數(shù)內(nèi),是否可以把前2個(gè)參數(shù)的相加過程,給抽離出來,因?yàn)閰?shù)都是相同的,沒必要每次都做運(yùn)算。
如果先不管函數(shù)內(nèi)的具體實(shí)現(xiàn),調(diào)用的寫法可以是這樣: sum(1, 2)(3); 或這樣 sum(1, 2)(10); 。就是,先把前2個(gè)參數(shù)的運(yùn)算結(jié)果拿到后,再與第3個(gè)參數(shù)相加。
這其實(shí)就是函數(shù)柯里化的簡(jiǎn)單應(yīng)用。
柯里化的實(shí)現(xiàn)sum(1, 2)(3); 這樣的寫法,并不常見。拆開來看,sum(1, 2) 返回的應(yīng)該還是個(gè)函數(shù),因?yàn)楹竺孢€有 (3) 需要執(zhí)行。
那么反過來,從最后一個(gè)參數(shù),從右往左看,它的左側(cè)必然是一個(gè)函數(shù)。以此類推,如果前面有n個(gè)(),那就是有n個(gè)函數(shù)返回了結(jié)果,只是返回的結(jié)果,還是一個(gè)函數(shù)。是不是有點(diǎn)遞歸的意思?
網(wǎng)上有一些不同的柯里化的實(shí)現(xiàn)方式,以下是個(gè)人覺得最容易理解的寫法:
function curry (fn, currArgs) { return function() { let args = [].slice.call(arguments); // 首次調(diào)用時(shí),若未提供最后一個(gè)參數(shù)currArgs,則不用進(jìn)行args的拼接 if (currArgs !== undefined) { args = args.concat(currArgs); } // 遞歸調(diào)用 if (args.length < fn.length) { return curry(fn, args); } // 遞歸出口 return fn.apply(null, args); } }
解析一下 curry 函數(shù)的寫法:
首先,它有 2 個(gè)參數(shù),fn 指的就是本文一開始的源處理函數(shù) sum。currArgs 是調(diào)用 curry 時(shí)傳入的參數(shù)列表,比如 (1, 2)(3) 這樣的。
再看到 curry 函數(shù)內(nèi)部,它會(huì)整個(gè)返回一個(gè)匿名函數(shù)。
再接下來的 let args = [].slice.call(arguments);,意思是將 arguments 數(shù)組化。arguments 是一個(gè)類數(shù)組的結(jié)構(gòu),它并不是一個(gè)真的數(shù)組,所以沒法使用數(shù)組的方法。我們用了 call 的方法,就能愉快地對(duì) args 使用數(shù)組的原生方法了。在這篇 「干貨」細(xì)說 call、apply 以及 bind 的區(qū)別和用法 中,有關(guān)于 call 更詳細(xì)的用法介紹。
currArgs !== undefined 的判斷,是為了解決遞歸調(diào)用時(shí)的參數(shù)拼接。
最后,判斷 args 的個(gè)數(shù),是否與 fn (也就是 sum )的參數(shù)個(gè)數(shù)相等,相等了就可以把參數(shù)都傳給 fn,進(jìn)行輸出;否則,繼續(xù)遞歸調(diào)用,直到兩者相等。
測(cè)試一下:
function sum(a, b, c) { console.log(a + b + c); } const fn = curry(sum); fn(1, 2, 3); // 6 fn(1, 2)(3); // 6 fn(1)(2, 3); // 6 fn(1)(2)(3); // 6
都能輸出 6 了,搞定!
柯里化的用途理解了柯里化的實(shí)現(xiàn)之后,讓我們來看一下它的實(shí)際應(yīng)用??吕锘哪康氖?,減少代碼冗余,以及增加代碼的可讀性。來看下面這個(gè)例子:
const persons = [ { name: "kevin", age: 4 }, { name: "bob", age: 5 } ]; // 這里的 curry 函數(shù),之前已實(shí)現(xiàn) const getProp = curry(function (obj, index) { const args = [].slice.call(arguments); return obj[args[args.length - 1]]; }); const ages = persons.map(getProp("age")); // [4, 5] const names = persons.map(getProp("name")); // ["kevin", "bob"]
在實(shí)際的業(yè)務(wù)中,我們常會(huì)遇到類似的列表數(shù)據(jù)。用 getProp 就可以很方便地,取出列表中某個(gè) key 對(duì)應(yīng)的值。
需要注意的是,const names = persons.map(getProp("name")); 執(zhí)行這條語句時(shí) getProp 的參數(shù)只有一個(gè) name,而定義 getProp 方法時(shí),傳入 curry 的參數(shù)有2個(gè),obj 和 index(這里必須寫 2 個(gè)及以上的參數(shù))。
為什么要這么寫?關(guān)鍵就在于 arguments 的隱式傳參。
const getProp = curry(function (obj, index) { console.log(arguments); // 會(huì)輸出4個(gè)類數(shù)組,取其中一個(gè)來看 // { // 0: {name: "kevin", age: 4}, // 1: 0, // 2: [ // {name: "kevin", age: 4}, // {name: "bob", age: 5} // ], // 3: "age" // } });
map 是 Array 的原生方法,它的用法如下:
var new_array = arr.map(function callback(currentValue[, index[, array]]) { // Return element for new_array }[, thisArg]);
所以,我們傳入的 name,就排在了 arguments 的最后。為了拿到 name 對(duì)應(yīng)的值,需要對(duì)類數(shù)組 arguments 做點(diǎn)轉(zhuǎn)換,讓它可以使用 Array 的原生方法。所以,最終 getProp 方法定義成了這樣:
const getProp = curry(function (obj, index) { const args = [].slice.call(arguments); return obj[args[args.length - 1]]; });
當(dāng)然,還有另外一種寫法,curry 的實(shí)現(xiàn)更好理解,但是調(diào)用的代碼卻變多了,大家可以根據(jù)實(shí)際情況進(jìn)行取舍。
const getProp = curry(function (key, obj) { return obj[key]; }); const ages = persons.map(item => { return getProp(item)("age"); }); const names = persons.map(item => { return getProp(item)("name"); });
最后,來看一個(gè) Memoization 的例子。它用于優(yōu)化比較耗時(shí)的計(jì)算,通過將計(jì)算結(jié)果緩存到內(nèi)存中,這樣對(duì)于同樣的輸入值,下次只需要中內(nèi)存中讀取結(jié)果。
function memoizeFunction(func) { const cache = {}; return function() { let key = arguments[0]; if (cache[key]) { return cache[key]; } else { const val = func.apply(null, arguments); cache[key] = val; return val; } }; } const fibonacci = memoizeFunction(function(n) { return (n === 0 || n === 1) ? n : fibonacci(n - 1) + fibonacci(n - 2); }); console.log(fibonacci(100)); // 輸出354224848179262000000 console.log(fibonacci(100)); // 輸出354224848179262000000
代碼中,第2次計(jì)算 fibonacci(100) 則只需要在內(nèi)存中直接讀取結(jié)果。
總結(jié)函數(shù)的柯里化,是 Javascript 中函數(shù)式編程的一個(gè)重要概念。它返回的,是一個(gè)函數(shù)的函數(shù)。其實(shí)現(xiàn)方式,需要依賴參數(shù)以及遞歸,通過拆分參數(shù)的方式,來調(diào)用一個(gè)多參數(shù)的函數(shù)方法,以達(dá)到減少代碼冗余,增加可讀性的目的。
雖然一開始理解起來有點(diǎn)云里霧里的,但一旦理解了其中的含義和具體的使用場(chǎng)景,用起來就會(huì)得心應(yīng)手了。
PS:歡迎關(guān)注我的公眾號(hào) “超哥前端小?!?,交流更多的想法與技術(shù)。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/101751.html
摘要:函數(shù)柯里化在函數(shù)式編程中,函數(shù)是一等公民。函數(shù)柯里化的主要作用和特點(diǎn)就是參數(shù)復(fù)用提前返回和延遲執(zhí)行。可能在實(shí)際應(yīng)用場(chǎng)景中,很少使用函數(shù)柯里化的解決方案,但是了解認(rèn)識(shí)函數(shù)柯里化對(duì)自身的提升還是有幫助的。 最近在整理面試資源的時(shí)候,發(fā)現(xiàn)一道有意思的題目,所以就記錄下來。 題目 如何實(shí)現(xiàn) multi(2)(3)(4)=24? 首先來分析下這道題,實(shí)現(xiàn)一個(gè) multi 函數(shù)并依次傳入?yún)?shù)執(zhí)行,...
摘要:什么是單頁面應(yīng)用單頁面應(yīng)用是指用戶在瀏覽器加載單一的頁面,后續(xù)請(qǐng)求都無需再離開此頁目標(biāo)旨在用為用戶提供了更接近本地移動(dòng)或桌面應(yīng)用程序的體驗(yàn)。流程第一次請(qǐng)求時(shí),將導(dǎo)航頁傳輸?shù)娇蛻舳?,其余?qǐng)求通過獲取數(shù)據(jù)實(shí)現(xiàn)數(shù)據(jù)的傳輸通過或遠(yuǎn)程過程調(diào)用。 什么是單頁面應(yīng)用(SPA)? 單頁面應(yīng)用(SPA)是指用戶在瀏覽器加載單一的HTML頁面,后續(xù)請(qǐng)求都無需再離開此頁 目標(biāo):旨在用為用戶提供了更接近本地...
摘要:什么是單頁面應(yīng)用單頁面應(yīng)用是指用戶在瀏覽器加載單一的頁面,后續(xù)請(qǐng)求都無需再離開此頁目標(biāo)旨在用為用戶提供了更接近本地移動(dòng)或桌面應(yīng)用程序的體驗(yàn)。流程第一次請(qǐng)求時(shí),將導(dǎo)航頁傳輸?shù)娇蛻舳?,其余?qǐng)求通過獲取數(shù)據(jù)實(shí)現(xiàn)數(shù)據(jù)的傳輸通過或遠(yuǎn)程過程調(diào)用。 什么是單頁面應(yīng)用(SPA)? 單頁面應(yīng)用(SPA)是指用戶在瀏覽器加載單一的HTML頁面,后續(xù)請(qǐng)求都無需再離開此頁 目標(biāo):旨在用為用戶提供了更接近本地...
摘要:忍者秘籍一書中,對(duì)于柯里化的定義如下在一個(gè)函數(shù)中首先填充幾個(gè)參數(shù)然后再返回一個(gè)新函數(shù)的技術(shù)稱為柯里化。回到我們的題目本身,其實(shí)根據(jù)測(cè)試用例我們可以發(fā)現(xiàn),函數(shù)的要求就是接受單一函數(shù),例如但是與柯里化不同之處在于,柯里化返回的一個(gè)新函數(shù)。 歡迎大家再一次來到我的文章專欄:從面試題中我們能學(xué)到什么,各位同行小伙伴是否已經(jīng)開始了悠閑的春節(jié)假期呢?在這里提前祝大家雞年大吉吧~哈哈,之前有人說...
閱讀 1668·2023-04-25 15:40
閱讀 3188·2021-08-11 11:15
閱讀 2420·2019-08-26 13:48
閱讀 2980·2019-08-26 12:18
閱讀 2601·2019-08-23 18:23
閱讀 3093·2019-08-23 17:01
閱讀 3119·2019-08-23 16:29
閱讀 1258·2019-08-23 15:15