柯里化(Currying),又稱部分求值(Partial Evaluation),是把接收多個參數的函數變成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受剩餘的參數並且返回結果的新函數的技術。
核心思想: 把多參數傳入的函數拆成單參數(或部分參數)函數,內部再返回調用下一個單參數(或部分參數)函數,依次處理剩餘的參數。javascript
按照Stoyan Stefanov --《JavaScript Pattern》做者 的說法,所謂柯里化就是使函數理解並處理部分應用。html
爲了實現只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩餘參數的這句話所描述的特徵。咱們先實現一個加法函數add
:前端
function add(x, y) { return x + y }
咱們如今實現一個被Currying的add
函數,命名該函數爲curriedAdd
,則根據上面的定義,curriedAdd
須要知足如下條件:vue
curriedAdd(1)(3) === 4 // true var increment = curriedAdd(1) increment(2) === 3 // true var addTen = curriedAdd(10) addTen(2) === 12 // true
知足以上條件的curriedAdd
函數能夠用如下代碼實現:java
function curriedAdd(x) { return function(y) { return x + y } }
固然以上實現有一些問題: 它不通用,而且咱們並不想經過修改函數被人的方式來實現Currying化。編程
可是curriedAdd
的實現代表了實現Currying的一個基礎--Currying延遲求值的特性須要咱們用到JavaScript
中的做用域,說得更通俗一些,咱們須要使用做用域(即閉包)來保存上一次傳進來的參數。瀏覽器
對curriedAdd
進行抽象,能夠獲得以下函數currying
:閉包
function currying (fn, ...args1) { return function (...args2) { return fn(...arg1, ...arg2) } } var increment = currying(add, 1) increment(2) === 3 // true var addTen = currying(add, 10) addTen(2) === 12 // true
在此實現中,currying
函數的返回值實際上是一個接受剩餘參數而且當即返回計算值的函數。即它的返回值並無自動被Currying。因此咱們能夠經過遞歸將currying
返回的函數也自動Currying。編程語言
function currying(fn, ...args) { if (args.length >= fn.length) { return fn(...args) } return function (...args2) { return currying(fn, ...args, ...args2) } }
以上函數很簡短,可是已經實現Currying的核心思想。JavaScript
中經常使用庫Lodash
中的curry
方法,其核心思想和以上並無太大差別--比較屢次接收的參數總數與函數定義時的形參數量,當接收的參數的數量大於或者等於被Currying函數的形參數量時,就返回運行結果,不然返回一個繼續接受參數的函數。函數式編程
固定不變的參數,實現參數複用是Currying的主要用途之一。
上文中的increment
、addTen
的一個參數複用的實例。對add
方法固定第一個參數爲10後,該方法就變成了一個將接受累加10的方法。
判斷對象的類型。例以下面這個例子:
function isArray (obj) { return Object.prototype.toString.call(obk) === '[object Array]' } function isNumber (obj) { return Object.prototype.toString.call(obj) === '[object Number]' } function isString (obj) { return Object.prototype.toString.call(obj) === '[object String]' } // Test isArray([1, 2, 3]) // true isNumber(123) // true isString('123') // true
可是上面方案有一個問題,那就是每種類型都須要定義一個方法,這裏咱們可使用bind
來擴展,優勢是能夠直接使用改造後的toStr
:
const toStr = Function.prototype.call.bind(Object.prototype.toString) // 改造前直接調用 [1, 2, 3].toString() // "1,2,3" '123'.toString() // "123" 123.toString() // SyntaxError: Invalid or unexpected token Object(123).toString() // "123" // 改造後調用 toStr toStr([1, 2, 3]) // "[object Array]" toStr('123') // "[object String]" toStr(123) // "[object Number]" toStr(Object(123)) // "[object Number]"
上面例子首先使用Function.prototype.call
函數指定一個this
值,而後.bind
返回一個新的函數,始終將Object.prototype.toString
設置爲傳入參數,其實等價於 Object.prototype.toString.call()
。
延遲執行也是Currying的一個重要使用場景,一樣bind
和箭頭函數
也能實現一樣的功能。
在前端開發中,一個常見的場景就是爲標籤綁定onClick
,同時考慮爲綁定的方法傳遞參數。
如下列出了幾種常見的方法,來比較優劣:
<div data-name="name" onClick={handleOnClick} />
經過data
屬性本質只能傳遞字符串的數據,若是須要傳遞複雜對象,只能經過 JSON.stringify(data)
來傳遞知足JSON
對象格式的數據,但對更加複雜的對象沒法支持。(雖然大多數時候也無需傳遞複雜對象)
<div onClick={handleOnClick.bind(null, data)} />
bind
方法和以上實現的currying 方法
,在功能上有極大的類似,在實現上也幾乎差很少。可能惟一的不一樣就是bind
方法須要強制綁定context
,也就是bind
的第一個參數會做爲原函數運行時的this
指向。而currying
不須要此參數。因此使用currying
或者bind
只是一個取捨問題。
<div onClick={() => handleOnClick(data))} />
箭頭函數可以實現延遲執行,同時也不像bind
方法必需指定context
。
<div onClick={currying(handleOnClick, data)} />
經過jsPerf
測試四種方式的性能,結果爲:箭頭函數
> bind
> currying
> trueCurrying
。currying
函數相比bind
函數,其原理類似,可是性能相差巨大,其緣由是bind
由瀏覽器實現,運行效率有加成。
若是咱們只是想提早綁定參數,那麼咱們有不少好幾個現成的選擇,bind
,箭頭函數
等,並且性能比Curring
更好。
Currying
是函數式編程的產物,它生於函數式編程,也服務於函數式編程。
而JavaScript
並不是真正的函數式編程語言,相比Haskell
等函數式編程語言,JavaScript
使用Currying
等函數式特性有額外的性能開銷,也缺少類型推導。
從而把JavaScript
代碼寫得符合函數式編程思想和規範的項目都較少,從而也限制了 Currying
等技術在JavaScript
代碼中的廣泛使用。
Currying
在JavaScript
中是低性能的,可是這些性能在絕大多數場景,是能夠忽略的。Currying
的思想極大地助於提高函數的複用性。Currying
生於函數式編程,也陷於函數式編程。假如沒有準備好寫純正的函數式代碼,那麼Currying
有更好的替代品。個人博客即將同步至騰訊雲+社區,邀請你們一同入駐:https://cloud.tencent.com/dev...