深入理解JavaScript柯里化的概念和原理
引言
在 JS
編程中, 函數(shù)是一等公民, 具備了強(qiáng)大的靈活性和復(fù)用性。而 柯里化
作為一種高階技術(shù), 可以進(jìn)一步提升函數(shù)的復(fù)用性和靈活性。通過柯里化, 可以大大簡化函數(shù)的調(diào)用方式, 并創(chuàng)建更加靈活和可復(fù)用的函數(shù)
在本篇博客中, 我們將深入探討 JS
中柯里化的概念和原理, 并了解其在實(shí)際開發(fā)中的應(yīng)用場景。通過學(xué)習(xí)柯里化, 您將能夠編寫出更加簡潔、可讀性更高且易于維護(hù)的代碼, 提升自己的 JS
編程技能
讓我們開始探索 JS
柯里化的奇妙世界吧!
一、什么是柯里化
柯里化
(Currying
), 又稱 部分求值
(Partial Evaluation
), 是函數(shù)編程的一種高級(jí)技巧, 通常只需要傳遞給函數(shù) 一部分參數(shù)
來調(diào)用它, 讓它返回一個(gè)新的函數(shù)去 處理剩下的參數(shù)
如下 ???? 代碼, sum
是一個(gè)常規(guī)求和函數(shù), sumCurry
則是使用 柯里化
思想編寫的一個(gè)求和函數(shù), 它們區(qū)別就很明顯了, sum
在執(zhí)行時(shí)需要一次性進(jìn)行傳參, 但是 sumCurry
則不是, 它可以進(jìn)行進(jìn)行 分批傳入?yún)?shù)
// 常規(guī)求和函數(shù) const sum = (a, b) => { console.log(a + b) } sum(1, 2) // 3
// 使用「柯里化」思想編寫的一個(gè)求和函數(shù) const sumCurry = (a) => (b) => { console.log(a + b) } sumCurry(1)(2) // 3
補(bǔ)充說明:
柯里化
是函數(shù)高級(jí)技巧, 允許分批次處理參數(shù), 很多文章更多討論的是如何將一個(gè)普通函數(shù)轉(zhuǎn)為柯里化
函數(shù), 我們需要將這兩者區(qū)分開來,柯里化
并不是指將一個(gè)函數(shù)從可調(diào)用的f(a, b, c)
轉(zhuǎn)換為可調(diào)用的f(a)(b)(c)
這個(gè)過程, 而是指轉(zhuǎn)換后的函數(shù)被稱為柯里化
二、柯里化作用
柯里化
的優(yōu)勢(shì)在于它為函數(shù)提供了更高的 靈活性
和 復(fù)用性
, 我們可以先提供部分函數(shù)參數(shù), 并在后續(xù)調(diào)用中根據(jù)需要提供剩余的參數(shù), 這種靈活性使得代碼更具可讀性、可維護(hù)性, 并且能夠創(chuàng)建具有不同功能的相關(guān)函數(shù), 下面我來看幾個(gè)具體的例子, 感受下 柯里化
的魅力
2.1 參數(shù)復(fù)用
如下代碼, sumCurry
是 柯里化
函數(shù), 同時(shí)存在變量 age
, 基于 age
我們調(diào)用了函數(shù) sumCurry
獲得到一個(gè)新的函數(shù) addAge
, 在后面我們可以隨時(shí)調(diào)用該函數(shù), 基于最初給的的參數(shù) age
追加數(shù)值
// 使用「柯里化」思想編寫的一個(gè)求和函數(shù) const sumCurry = (a) => (b) => { console.log(a + b) } const age = 18 const addAge = sumCurry(age) addAge(1) // 19 addAge(10) // 28 addAge(8) // 26 addAge(0) // 18
2.2 函數(shù)復(fù)用
下面我們有這么一個(gè)需求, 需要抽取對(duì)象數(shù)組中 name
屬性, 并返回一個(gè)數(shù)組, 可實(shí)現(xiàn)代碼如下:
const users = [ { name: 'lh', age: 18 }, { name: 'myj', age: 28 }, { name: 'jl', age: 20 }, ] const names = users.map(v => v.name) console.log(names) // ['lh', 'myj', 'jl']
上面 ???? 代碼通過 map
來抽取數(shù)組對(duì)象中的 name
的屬性, 接下來我們嘗試使用 柯里化
對(duì)代碼進(jìn)行簡單的優(yōu)化:
// 使用「柯里化」創(chuàng)建一個(gè)提供 map 使用的、可復(fù)用函數(shù) const prop = (key) => (obj) => obj[key] const users = [ { name: 'lh', age: 18 }, { name: 'myj', age: 28 }, { name: 'jl', age: 20 }, ] const names = users.map(prop('name')) console.log(names) // ['lh', 'myj', 'jl']
上面代碼很簡單, 但卻是 柯里化
的一個(gè)實(shí)踐, 有了 prop
函數(shù), 我們就可以很方便的, 抽取對(duì)象數(shù)組中指定元素, 而且在代碼閱讀上也相對(duì)來說清晰很多, 比如在上面基礎(chǔ)之上我們想要獲取所有 age
只需要這么編寫即可:
const ages = users.map(prop('age'))
2.3 提前計(jì)算
如下代碼, 是常見的一種兼容性代碼的寫法, 代碼中通過判斷 addEventListener
以及 attachEvent
為現(xiàn)代瀏覽器或 IE
瀏覽器的事件添加方法
const addEvent = (el, type, fn, capture) => { if (window.addEventListener) { el.addEventListener(type, (e) => { fn.call(el, e); }, capture); } else if (window.attachEvent) { el.attachEvent("on" + type, (e) => { fn.call(el, e); }); } };
上面 ???? 代碼寫法沒啥毛病, 中規(guī)中距, 唯一的毛病可能就是在每次執(zhí)行 addEvent
時(shí)都需要進(jìn)行一次判斷, 這里我們就可以通過 柯里化
改造下, 提前
對(duì)兼容性進(jìn)行判斷, 最后返回添加事件的一個(gè) 主體方法
// 柯里化, 提前計(jì)算, 返回主體方法 const useEvent = () => { if (window.addEventListener) { return (el, sType, fn, capture) => { el.addEventListener(sType, (e) => { fn.call(el, e); }, (capture)); }; } else if (window.attachEvent) { return (el, sType, fn, capture) => { el.attachEvent("on" + sType, (e) => { fn.call(el, e); }); }; } } const addEvent = useEvent()
2.4 延遲計(jì)算(運(yùn)行)
延遲計(jì)算其實(shí)也好理解, 就是每次函數(shù)執(zhí)行都會(huì) 收集一部分參數(shù)
, 直到所有參數(shù)都準(zhǔn)備完畢, 才會(huì)進(jìn)行計(jì)算, 如下代碼:
// 使用「柯里化」思想編寫的一個(gè)求和函數(shù) const sumCurry = (a) => (b) => (c) => (d) => { console.log(a + b + c + d) } sumCurry(1)(2)(3)(4) // 10
三、普通函數(shù)如果轉(zhuǎn)換為「柯里化」
接下來我們嘗試寫一個(gè)轉(zhuǎn)換函數(shù)
curry
, 通過它我們可以將普通函數(shù)f(a, b, c, d)
轉(zhuǎn)為柯里化函數(shù)f(a)(b)(c)
, 轉(zhuǎn)換后柯里化
函數(shù)的精髓
在于: 接收一部分參數(shù), 返回一個(gè)函數(shù)接收剩余參數(shù), 當(dāng)接收到足夠參數(shù)后, 執(zhí)行原函數(shù)
如下代碼所示, 我們實(shí)現(xiàn)了簡單的一個(gè)轉(zhuǎn)換函數(shù) curry
, _curry
是個(gè)中轉(zhuǎn)函數(shù), 也是重點(diǎn):
_curry
本身就是個(gè)柯里化
函數(shù), 它接收一個(gè)原函數(shù)
、以及當(dāng)前已經(jīng)接收到的參數(shù)
_curry
返回一個(gè)新的函數(shù)
, 用于接收剩余的參數(shù)
- 新函數(shù)接收若干參數(shù), 內(nèi)部會(huì)將之前收集的參數(shù)以及接收到的參數(shù)進(jìn)行合并, 并對(duì)
參數(shù)個(gè)數(shù)進(jìn)行判斷
, 如果接收到足夠的參數(shù)了, 則執(zhí)行原函數(shù), 如果接收的參數(shù)不夠則執(zhí)行_curry
函數(shù), 并返回一個(gè)新的函數(shù)繼續(xù)接收處理剩余的參數(shù)
/** * 中轉(zhuǎn)函數(shù) * @param fun 待柯里化的原函數(shù) * @param allArgs 已接收的參數(shù)列表 * @returns {Function} 返回一個(gè)接收剩余參數(shù)的函數(shù) */ const _curry = (fun, ...allArgs) => { // 1. 返回一個(gè)接收剩余參數(shù)的函數(shù) return (...currentArgs) => { // 2. 當(dāng)前接收到的所有參數(shù) const _args = [...allArgs, ...currentArgs] // 3. 接收到的參數(shù)大于或等于函數(shù)本身的參數(shù)時(shí), 執(zhí)行原函數(shù) if (_args.length >= fun.length) { return fun.call(this, ..._args) } // 4. 繼續(xù)執(zhí)行 _curry 返回一個(gè)接收剩余參數(shù)的函數(shù) return _curry.call(this, fun, ..._args) } } /** * 將函數(shù)柯里化 * @param fun 待柯里化的原函數(shù) * @returns {Function} 返回「柯里化」函數(shù) */ const curry = (fun) => _curry.call(this, fun) // 測(cè)試 const sum = (a, b, c) => (a + b + c) // 原函數(shù) const currySum = curry(sum) // 柯里化 函數(shù) currySum(1)(2)(3) // 6 currySum(1)(2, 3) // 6 currySum(1, 2, 3) // 6
四、參數(shù)個(gè)數(shù)不定
下面我們回到 柯里化
本身, 還記得我們上文寫的 sumCurry
函數(shù)嗎? 該 柯里化
函數(shù)每次調(diào)用都只能傳一個(gè)參數(shù)
// 使用「柯里化」思想編寫的一個(gè)求和函數(shù) const sumCurry = (a) => (b) => { return a + b } sumCurry(1)(2) // 3
這里我們對(duì) sumCurry
進(jìn)行改造, 允許在每次調(diào)用時(shí)傳遞任意個(gè)數(shù)參數(shù)
// 使用「柯里化」思想編寫的一個(gè)求和函數(shù) const sumCurry = (...preArgs) => (...nextArgs) => { return [...preArgs, ...nextArgs].reduce((total, ele) => (total + ele), 0) } sumCurry(1, 2, 3)() // 6 sumCurry(1, 2, 3)(4) // 10 sumCurry()(1, 4) // 5 sumCurry(1)(2) // 3
五、無限參數(shù)
上文我們所演示的 柯里化
函數(shù)允許被 連續(xù)調(diào)用次數(shù)
是固定的, 如果我們要實(shí)現(xiàn)一個(gè)可以 無限調(diào)用
的柯里化函數(shù)又該怎么處理呢? 實(shí)現(xiàn)原理其實(shí)也簡單, 重點(diǎn)就以下兩點(diǎn):
- 只需要寫個(gè)
循環(huán)調(diào)用
即可, 函數(shù)每次返回新函數(shù), 新函數(shù)調(diào)用原函數(shù)并將上一次結(jié)果傳給原函數(shù) - 修改函數(shù)
toString
方法, 用于取值
// 使用「柯里化」思想編寫的一個(gè)求和函數(shù) const sumCurry = (...preArgs) => { // 1. 計(jì)算當(dāng)前結(jié)果 const preTotal = preArgs.reduce((total, ele) => (total + ele), 0) // 2. 使用 bind, 基于原函數(shù)創(chuàng)建新函數(shù), 并將上一次結(jié)果作為第一個(gè)參數(shù) const _sumCurry = sumCurry.bind(this, preTotal) // 3. 修改 toString 返回值, 用于值值 _sumCurry.toString = () => preTotal return _sumCurry } // 使用 + 強(qiáng)制調(diào)用 toString 方法 console.log(+sumCurry(1)) // 1 console.log(+sumCurry(1, 2, 3)) // 6 console.log(+sumCurry(1)(2)) // 3 console.log(+sumCurry(1)(2)(3)(4)(5)) // 15 console.log(+sumCurry(1)()(3)(4)(5)) // 13
六、柯里化和閉包的關(guān)系
閉包
和 柯里化
是兩個(gè)不同的概念, 但它們之間有一定的關(guān)聯(lián):
在編程中, 閉包允許函數(shù)捕獲并訪問其定義時(shí)的上下文中的變量, 即使在其定義環(huán)境之外被調(diào)用時(shí)也可以使用這些變量, 閉包可以通過函數(shù)返回函數(shù)的方式創(chuàng)建, 從而使得內(nèi)部函數(shù)可以訪問外部函數(shù)的變量
柯里化是指高階函數(shù)使用技巧, 柯里化函數(shù)的特點(diǎn)是, 允許被連續(xù)調(diào)用
f(a)(b)(c)
每次調(diào)用傳遞若干參數(shù), 同時(shí)返回一個(gè)接受剩余參數(shù)
的新函數(shù)
, 直到所有參數(shù)都被傳遞完畢, 才會(huì)執(zhí)行主體邏輯閉包和柯里化之間的關(guān)系在于,
柯里化函數(shù)
通常會(huì)使用閉包
來實(shí)現(xiàn)
, 當(dāng)我們將一個(gè)函數(shù)進(jìn)行柯里化時(shí), 每次返回一個(gè)新的函數(shù), 這個(gè)新的函數(shù)會(huì)捕獲前一次調(diào)用時(shí)的參數(shù)和上下文, 這個(gè)上下文就形成了閉包, 使得新函數(shù)可以在后續(xù)調(diào)用中繼續(xù)訪問之前傳遞的參數(shù)和上下文
// 形成閉包, 返回的新函數(shù), 允許訪問父級(jí)函數(shù)的變量 a const sumCurry = (a) => (b) => { console.log(a + b) }
到此這篇關(guān)于深入理解JavaScript柯里化的概念和原理的文章就介紹到這了,更多相關(guān)JavaScript 柯里化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript用select實(shí)現(xiàn)日期控件
這篇文章主要介紹了JavaScript用select實(shí)現(xiàn)日期控件的相關(guān)資料,需要的朋友可以參考下2015-07-07JavaScript實(shí)現(xiàn)非常簡單實(shí)用的下拉菜單效果
這篇文章主要介紹了JavaScript實(shí)現(xiàn)非常簡單實(shí)用的下拉菜單效果,通過定義顯示及隱藏菜單項(xiàng)及鼠標(biāo)事件調(diào)用該函數(shù)實(shí)現(xiàn)下拉菜單功能,需要的朋友可以參考下2015-08-08微信小程序自定義可滑動(dòng)頂部TabBar選項(xiàng)卡實(shí)現(xiàn)頁面切換功能示例
這篇文章主要介紹了微信小程序自定義可滑動(dòng)頂部TabBar選項(xiàng)卡實(shí)現(xiàn)頁面切換功能,結(jié)合實(shí)例形式分析了微信小程序自定義頂部TabBar選項(xiàng)卡頁面切換功能的相關(guān)布局、樣式及功能實(shí)現(xiàn)技巧,需要的朋友可以參考下2019-05-05使用JavaScript刪除HTML元素的2種方法及3種情況
給定一個(gè)HTML元素,如何使用JavaScript從文檔中刪除該HTML元素,這篇文章主要給大家介紹了關(guān)于使用JavaScript刪除HTML元素的2種方法及3種情況,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01IE6/7 and IE8/9/10(IE7模式)依次隱藏具有absolute或relative的父元素和子元素后再顯示
多數(shù)情況下隱藏(設(shè)置display:none)一個(gè)元素,無需依次將其內(nèi)的所有子元素都隱藏。非要這么做,有時(shí)會(huì)碰到意想不到的bug。2011-07-07基于javascript實(shí)現(xiàn)表格的簡單操作
這篇文章主要為大家詳細(xì)介紹了基于javascript實(shí)現(xiàn)表格的簡單操作,具有一定的參考價(jià)值,感興趣的朋友可以參考一下2016-05-05javascript 用函數(shù)語句和表達(dá)式定義函數(shù)的區(qū)別詳解
本篇文章主要介紹了javascript 用函數(shù)語句和表達(dá)式定義函數(shù)的區(qū)別。需要的朋友可以過來參考下,希望對(duì)大家有所幫助2014-01-01