一、什麼是柯里化
Currying ——只傳遞給函數一部分參數來進行調用,並讓它返回一個函數去處理剩下的參數。
柯里化即 Currying,是一門編譯原理層面的技術,用途是實現多參函數,其爲實現多參函數提供了一個遞歸降解的實現思路——把接受多個參數的函數變換成接受第一個參數的函數,並且返回接受剩餘參數且返回結果的新函數。
某些編程語言中(如 Haskell)就是通過柯里化技術支持多參函數這一語言特性的
二、JS 柯里化的實現
先來寫一個實現加法的函數 add
:
function add(x, y) {
return (x + y)
}
現在我們直接實現一個被柯里化的 add
函數,該函數名爲 curriedAdd
,則根據上面的定義,curriedAdd
需要滿足以下條件:
curriedAdd(1)(3) === 4
// true
var increment = curriedAdd(1)
increment(2) === 3
// true
var addTen = curriedAdd(10)
addTen(2) === 12
// true
滿足以上條件的 curriedAdd
的函數可以用以下代碼段實現:
function curriedAdd(x) {
return function (y) {
return x + y
}
}
這種實現方式並不通用,但表明了實現柯里化的一個基礎——柯里化延遲求值的特性需要用到 JavaScript 中的作用域——使用作用域來保存上一次傳進來的參數。
對 curriedAdd
進行抽象,可以得到如下函數 currying
:
function currying(fn, ...args1) {
return function (...args2) {
return fn(...args1, ...args2)
}
}
var increment = currying(add, 1)
increment(2) === 3
// true
var addTen = currying(add, 10)
addTen(2) === 12
// true
在此實現中,currying
函數的返回值其實是一個接收剩餘參數並立即返回計算值的函數,即它的返回值並沒有自動被柯里化 。
所以我們可以通過遞歸來將柯里化返回的函數也自動柯里化。
function trueCurrying(fn, ...args) {
if (args.length >= fn.length) {
return fn(...args)
}
return function (...args2) {
return trueCurrying(fn, ...args, ...args2)
}
}
以上函數實現了柯里化的核心思想。
JavaScript 中的常用庫 Lodash 中的 curry 方法,其核心思想和以上相似,都是對比多次接受的參數總數與函數定義時的入參數量,當接受參數的數量大於或等於被柯里化函數的傳入參數數量時,就返回計算結果,否則返回一個繼續接受參數的函數。
三、柯里化的應用
1、參數複用
固定不變的參數,實現參數複用是柯里化的主要用途之一:
var increment = curriedAdd(1)
increment(2) === 3
// true
var addTen = curriedAdd(10)
addTen(2) === 12
// true
上面的 increment
, addTen
就是進行參數複用
2、延遲執行
延遲執行也是柯里化的一個重要使用場景,而 bind 和箭頭函數也能實現同樣的功能。
一個常見的場景就是爲標籤綁定 onClick 事件,同時考慮爲綁定的方法傳遞參數。
以下列出了幾種常見的方法,來比較優劣:
① 通過 data 屬性
<div data-name="name" onClick={handleOnClick} />
通過 data 屬性本質只能傳遞字符串的數據,如果需要傳遞複雜對象,只能通過 JSON.stringify(data)
來傳遞滿足 JSON 對象格式的數據,但對更加複雜的對象無法支持
② 通過 bind 方法
<div onClick={handleOnClick.bind(null, data)} />
bind 方法和以上實現的 currying
方法,在功能上有極大的相似,在實現上也幾乎差不多。
唯一的不同就是 bind 方法需要強制綁定 context,即 bind 的第一個參數會作爲原函數運行時的 this 指向,
而 currying
不需要此參數。
③ 通過箭頭函數
<div onClick={() => handleOnClick(data)} />
箭頭函數能夠實現延遲執行,同時也不像 bind 方法必需指定 context。
但在 react 中,不建議直接在 jsx 標籤內寫箭頭函數(直接在 jsx 標籤內寫業務邏輯)。
④ 通過 currying
<div onClick={currying(handleOnClick, data)} />
⑤ 性能對比
通過 jsPerf
測試四種方式的性能,結果爲:箭頭函數
>bind
>currying
>trueCurrying
currying 函數相比 bind 函數,其原理相似,但是性能相差巨大,其原因是 bind 由瀏覽器實現,運行效率有加成。
從這個結果看柯里化性能是最差的,但另一方面就算最差的 trueCurrying
的實現,也能達到 50w Ops/s,說明這些性能其實影響非常小。
而 trueCurrying
方法中實現的自動柯里化,是另外三個方法所不具備的。
四、柯里化的優劣勢
1、優勢
① 爲了多參函數複用性
柯里化讓人眼前一亮的地方在於,讓人覺得函數還能這樣子複用。
通過一行代碼,將 add 函數轉換爲 increment,addTen 等。
對於柯里化的複雜實現中,以 Lodash 爲列,提供了 placeholder 對多參函數的複用玩出花樣:
import _ from 'loadsh'
function abc(a, b, c) {
return [a, b, c];
}
var curried = _.curry(abc)
// Curried with placeholders.
curried(1)(_, 3)(2)
// => [1, 2, 3]
② 爲函數式編程而生
柯里化是爲函數式而生的東西,應運着有一整套函數式編程的東西,純函數、compose、container 等等。
假如要寫 Pointfree Javascript 風格的代碼,那麼柯里化是不可或缺的。使用 compose、container 等也需要柯里化
2、劣勢
① 柯里化的一些特性有其他解決方案
如果我們只是想提前綁定參數,那麼我們有很多好幾個現成的選擇,bind,箭頭函數等,而且性能比柯里化更好
② 柯里化陷於函數式編程
上面 trueCurrying 的實現是最符合柯里化定義的,也提供了 bind,箭頭函數等不具備的“新奇”特性——可持續的柯里化。
但柯里化是函數式編程的產物,它生於函數式編程,也服務於函數式編程,而 JavaScript 並非真正的函數式編程語言,相比 Haskell 等函數式編程語言,JavaScript 使用柯里化等函數式特性有額外的性能開銷,也缺乏類型推導。
假如沒有準備好去寫函數式編程規範的代碼,僅需要在 JSX 代碼中提前綁定一次參數,那麼 bind 或箭頭函數就足夠了。
五、總結
1、柯里化在 JavaScript 中是“低性能”的,但是這些性能在絕大多數場景,是可以忽略的。
2、柯里化的思想極大地助於提升函數的複用性。
3、柯里化生於函數式編程,也陷於函數式編程。