柯里化(Currying)
柯里化(Currying)[1]是一種關於函數的高階技術。它不僅被用於 JavaScript,還被用於其他編程語言。
柯里化是一種函數的轉換,它是指將一個函數從可調用的 f(a, b, c)
轉換為可調用的 f(a)(b)(c)
。
柯里化不會調用函數。它只是對函數進行轉換。
讓我們先來看一個例子,以更好地理解我們正在講的內容,然后再進行一個實際應用。
我們將創建一個輔助函數 curry(f)
,該函數將對兩個參數的函數 f
執行柯里化。換句話說,對於兩個參數的函數 f(a, b)
執行 curry(f)
會將其轉換為以 f(a)(b)
形式運行的函數:
正如你所看到的,實現非常簡單:只有兩個包裝器(wrapper)。
-
curry(func)
的結果就是一個包裝器function(a)
。 -
當它被像
curriedSum(1)
這樣調用時,它的參數會被保存在詞法環境中,然后返回一個新的包裝器function(b)
。 -
然后這個包裝器被以
2
為參數調用,並且,它將該調用傳遞給原始的sum
函數。
柯里化更高級的實現,例如 lodash 庫的 _.curry[2],會返回一個包裝器,該包裝器允許函數被正常調用或者以偏函數(partial)的方式調用:
柯里化?目的是什么?
要了解它的好處,我們需要一個實際中的例子。
例如,我們有一個用於格式化和輸出信息的日志(logging)函數 log(date, importance, message)
。在實際項目中,此類函數具有很多有用的功能,例如通過網絡發送日志(log),在這兒我們僅使用 alert
:
所以:
-
柯里化之后,我們沒有丟失任何東西:
log
依然可以被正常調用。 -
我們可以輕松地生成偏函數,例如用於生成今天的日志的偏函數。
高級柯里化實現
如果你想了解更多細節,下面是用於多參數函數的“高級”柯里化實現,我們也可以把它用於上面的示例。
它非常短:
用例:
當我們運行它時,這里有兩個 if
執行分支:
-
現在調用:如果傳入的
args
長度與原始函數所定義的(func.length
)相同或者更長,那么只需要將調用傳遞給它即可。 -
獲取一個偏函數:否則,
func
還沒有被調用。取而代之的是,返回另一個包裝器pass
,它將重新應用curried
,將之前傳入的參數與新的參數一起傳入。然后,在一個新的調用中,再次,我們將獲得一個新的偏函數(如果參數不足的話),或者最終的結果。
例如,讓我們看看 sum(a, b, c)
這個例子。它有三個參數,所以 sum.length = 3
。
對於調用 curried(1)(2)(3)
:
-
第一個調用
curried(1)
將1
保存在詞法環境中,然后返回一個包裝器pass
。 -
包裝器
pass
被調用,參數為(2)
:它會獲取之前的參數(1)
,將它與得到的(2)
連在一起,並一起調用curried(1, 2)
。由於參數數量仍小於 3,curry
函數依然會返回pass
。 -
包裝器
pass
再次被調用,參數為(3)
,在接下來的調用中,pass(3)
會獲取之前的參數 (1
,2
) 並將3
與之合並,執行調用curried(1, 2, 3)
— 最終有3
個參數,它們被傳入最原始的函數中。
如果這還不夠清楚,那你可以把函數調用順序在你的腦海中或者在紙上過一遍。
總結
柯里化 是一種轉換,將 f(a,b,c)
轉換為可以被以 f(a)(b)(c)
的形式進行調用。JavaScript 實現通常都保持該函數可以被正常調用,並且如果參數數量不足,則返回偏函數。
柯里化讓我們能夠更容易地獲取偏函數。就像我們在日志記錄示例中看到的那樣,普通函數 log(date, importance, message)
在被柯里化之后,當我們調用它的時候傳入一個參數(如 log(date)
)或兩個參數(log(date, importance)
)時,它會返回偏函數。
現代 JavaScript 教程:開源的現代 JavaScript 從入門到進階的優質教程。React 官方文檔推薦,與 MDN 並列的 JavaScript 學習教程[3]。
在線免費閱讀:https://zh.javascript.info
參考資料
[1]
柯里化(Currying): https://en.wikipedia.org/wiki/Currying
[2]
_.curry: https://lodash.com/docs#curry
[3]
React 官方文檔推薦,與 MDN 並列的 JavaScript 學習教程: https://zh-hans.reactjs.org/docs/getting-started.html#javascript-resources