前言
我們?cè)诟鞣N算法題以及技術(shù)文檔中經(jīng)常會(huì)看到柯里化這個(gè)詞懦尝,那么知纷,柯里化到底是什么?它在js中如何運(yùn)用陵霉?對(duì)我們的編程有什么作用琅轧?都9102年了,如果你還不知道這些踊挠,那么你在面試過(guò)程中很可能會(huì)被面試官diss??
什么是柯里化(Currying)
維基百科的解釋是:把接收多個(gè)參數(shù)的函數(shù)變換成接收一個(gè)單一參數(shù)(最初函數(shù)的第一個(gè)參數(shù))的函數(shù)乍桂,并返回接受剩余的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)。其由數(shù)學(xué)家Haskell Brooks Curry提出效床,并以curry命名睹酌。
簡(jiǎn)單的說(shuō),柯里化函數(shù)持續(xù)地返回一個(gè)新函數(shù)直到所有的參數(shù)用盡為止剩檀。這些參數(shù)全部保持“活著”的狀態(tài)(通過(guò)閉包)忍疾,然后當(dāng)柯里化鏈中的最后一個(gè)函數(shù)被返回和執(zhí)行時(shí)會(huì)全部被用來(lái)執(zhí)行。
這和高階組件(Higher-order functions)如出一轍谨朝。前者返回一個(gè)新函數(shù)卤妒,后者返回一個(gè)新組件甥绿。
舉個(gè)簡(jiǎn)單的栗子
本例使用到的部分ES6知識(shí):const,arrow function则披。不了解的同學(xué)可先行查看共缕。
寫(xiě)一個(gè)計(jì)算三個(gè)參數(shù)相乘的函數(shù):
function multiply (a, b, c) {
return a * b * c
}
multiply(1, 2, 3) // 6
這是我們第一反應(yīng)寫(xiě)出來(lái)的demo,也是看起來(lái)最簡(jiǎn)單的實(shí)現(xiàn)方法士复。再來(lái)創(chuàng)建一個(gè)柯里化版本的函數(shù):
function multiply (a) {
return (b) => {
return (c) => {
return a * b * c
}
}
}
multiply(1)(2)(3) // 6
這里我們將multiply(1图谷,2,3)調(diào)用變成了multiply (1) (2) (3) 調(diào)用阱洪。
單獨(dú)一個(gè)函數(shù)被轉(zhuǎn)換成了一系列函數(shù)便贵。為了得到數(shù)字1、2冗荸、3相乘的結(jié)果承璃,這些數(shù)字被一個(gè)接一個(gè)地傳遞,每個(gè)數(shù)字預(yù)填了下一個(gè)函數(shù)內(nèi)聯(lián)調(diào)用蚌本。
我們把multiply (1) (2) (3) 分割一下來(lái)幫助理解:
const mul1 = multiply(1)
const mul2 = mul1(2)
const result = mul2(3)
console.log(result) // 6
當(dāng)mul2使用3作為參數(shù)調(diào)用時(shí)盔粹,它一起使用了之前已拿到的參數(shù)a=1和b=2進(jìn)行運(yùn)算并返回結(jié)果6。
作為一個(gè)嵌套函數(shù)程癌,mul2能夠訪問(wèn)到外部的兩個(gè)函數(shù)multiply和mul1的作用域舷嗡。這就是為什么mul2能利用定義在已經(jīng)‘離場(chǎng)’的函數(shù)中的參數(shù)來(lái)進(jìn)行乘法操作的原因。即使這些函數(shù)早已返回并且從內(nèi)存中垃圾回收了嵌莉,但其變量仍然保持‘活著’(閉包)进萄。你可以看到3個(gè)數(shù)字每次只有1個(gè)提供給函數(shù),并且同一時(shí)間里一個(gè)新函數(shù)會(huì)被返回锐峭,直到所有的數(shù)字用盡為止垮斯。
柯里化背后的邏輯就是獲取一個(gè)函數(shù)并派生出一個(gè)返回特殊函數(shù)的函數(shù),它實(shí)際上是一種思想只祠,或者說(shuō)是一種程序設(shè)計(jì)模式兜蠕。
柯里化的應(yīng)用
1. 編寫(xiě)可以輕松復(fù)用和配置的小代碼塊,就像我們使用npm一樣:
舉個(gè)例子抛寝,你有一家商店熊杨,然后你想給你的優(yōu)惠顧客10%的折扣:
function discount (price, discount) {
return price * discount
}
// 當(dāng)一個(gè)顧客消費(fèi)了500元
const price = discount(500, 0.1) // $50
// 從長(zhǎng)遠(yuǎn)看,你的每一筆生意都要計(jì)算10%的折扣
const price = discount(1500, 0.1) // $150
const price = discount(2000, 0.1) // $200
const price = discount(50, 0.1) // $5
const price = discount(300, 0.1) // $30
// 將這個(gè)函數(shù)柯里化盗舰,然后我們就不用每次都寫(xiě)那0.1了
function discount (discount) {
return (price) => {
return price * discount
}
}
const tenPercentDiscount = discount(0.1)
// 現(xiàn)在晶府,我們只需用商品價(jià)格來(lái)計(jì)算就可以了:
tenPercentDiscount(500) // $50
// 接下來(lái),有些優(yōu)惠顧客越來(lái)越重要钻趋,讓我們稱(chēng)為vip顧客川陆,然后我們要給20%的折扣,我們這樣來(lái)使用柯里化了的discount函數(shù):
const twentyPercentDiscount = discount(0.2)
// 我們?yōu)関ip顧客使用0.2調(diào)用柯里化discount函數(shù)來(lái)配置了一個(gè)新的函數(shù)蛮位。這個(gè)twentyPercentDiscount函數(shù)會(huì)被用來(lái)計(jì)算vip顧客的折扣:
twentyPercentDiscount(500) // $100
twentyPercentDiscount(3000) // $600
twentyPercentDiscount(80000) // $16000
這個(gè)例子說(shuō)明较沪,使用柯里化思想能讓我們?cè)谟龅街荒艽_定一個(gè)參數(shù)而無(wú)法確定另一個(gè)參數(shù)時(shí)鳞绕,代碼設(shè)計(jì)編的變得更方便與高效,達(dá)到提升性能的目的尸曼。
2.避免頻繁調(diào)用具有相同參數(shù)的函數(shù):
比如我們有個(gè)用來(lái)計(jì)算體積的函數(shù):
function volume (l, w, h) {
return l * w * h
}
// 碰巧你倉(cāng)庫(kù)里的所有物品都是100m高们何。你會(huì)看到你不停地用h=100來(lái)調(diào)用這個(gè)函數(shù):
volume(200, 30, 100) // 2003000
volume(32, 45, 100) // 144000
volume(2322, 232, 100) // 53870400
// 為了解決這個(gè)問(wèn)題,你把volume函數(shù)柯里化(像我們之前做過(guò)的):
function volume (h) {
return (w) => {
return (l) => {
return l * w * h
}
}
}
// 我們能給同類(lèi)物品定義一個(gè)特殊函數(shù):
const hCylinderHeight = volume(100)
hCylinderHeight(200)(30) // 600000
hCylinderHeight(2322)(232) // 53870400
通用的柯里函數(shù)
讓我們建立一個(gè)函數(shù)來(lái)接受任何函數(shù)并且返回柯里化版本的函數(shù):
function curry (fn, ...args) {
return (..._args) => {
return fn(...args, ..._args)
}
}
我們?cè)谶@里做了什么控轿?我們的curry函數(shù)接受一個(gè)我們想要柯里化的函數(shù)(fn)和一個(gè)變量(...args)冤竹。這里的rest操作符用來(lái)將參數(shù)聚集成一個(gè)...args。接下來(lái)我們返回一個(gè)函數(shù)茬射,該函數(shù)將其余參數(shù)收集為...args鹦蠕。此函數(shù)通過(guò)spread運(yùn)算符將... args和... args作為參數(shù)解構(gòu)傳入來(lái)調(diào)用原始函數(shù)fn,然后將值返回給用戶(hù)在抛。
讓我們使用我們的curry函數(shù)用之前的例子來(lái)創(chuàng)建一個(gè)特殊的函數(shù)(一個(gè)專(zhuān)門(mén)用來(lái)計(jì)算100m長(zhǎng)度的物品體積):
function volume (l, h, w) {
return l * h * w
}
const hCy = curry(volume, 100)
hCy(200, 900) // 18000000
hCy(70, 60) // 420000
將類(lèi)似回調(diào)函數(shù)的參數(shù)傳入柯里化函數(shù)钟病,能使復(fù)雜的問(wèn)題變得簡(jiǎn)單!
使用遞歸實(shí)現(xiàn)curry函數(shù)
JS柯里化作為函數(shù)式編程的重要一環(huán)霜定,頻繁在算法題中出現(xiàn)。以上的通用柯里化函數(shù)還不夠完善廊鸥,我們希望只給curry函數(shù)傳遞一個(gè)fn就能達(dá)到目的望浩,現(xiàn)在我們使用遞歸來(lái)實(shí)現(xiàn):
function curry (fn) {
const c = (...args) => (args.length === fn.length) ?
fn(...args) : (..._args) => c(...args, ..._args)
return c
}
該方法幾乎為最簡(jiǎn)潔、代碼行數(shù)最少的實(shí)現(xiàn)方法了惰说。
首先我們能確定磨德,實(shí)現(xiàn)柯里化的核心就是要確定傳入?yún)?shù)的個(gè)數(shù),并通通取到吆视。
其次典挑,我們得知道,fn.length
為fn函數(shù)接受的參數(shù)個(gè)數(shù)啦吧,那么該實(shí)現(xiàn)方法就能解讀為:
不斷遞歸獲取傳入?yún)?shù)您觉,直到取到的參數(shù)個(gè)數(shù)等于fn的參數(shù)個(gè)數(shù)為止,最終將獲取到的所有參數(shù)傳給fn并返回執(zhí)行結(jié)果授滓。
結(jié)語(yǔ)
柯里化作為一種重要的程序思想琳水,已經(jīng)廣為應(yīng)用,它使我們應(yīng)對(duì)復(fù)雜問(wèn)題時(shí)能提升效率般堆,增強(qiáng)可讀性在孝。希望讀者都能體會(huì)這種思想并運(yùn)用于實(shí)踐,相信在提升技術(shù)的路上能越走越遠(yuǎn)淮摔,成為一名優(yōu)秀的工程師私沮!