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