高階函式技巧-函式柯里化
我們經常說在Javascript語言中,函式
是“一等公民”,它們本質上是十分簡單和過程化的。可以利用函式,進行一些簡單的資料處理,return
結果,或者有一些額外的功能,需要通過使用閉包來實現,最後經常會return
匿名函式。
如果你對函數語言程式設計有一定了解,函式柯里化 (function currying)是不可或缺的,利用函式柯里化,可以在開發中非常優雅的處理複雜邏輯。
函式柯里化
柯里化(Currying),維基百科上的解釋是,把接受多個引數的函式轉換成接受一個單一引數的函式
先看一個簡單例子
// 柯里化 var foo = function(x) { return function(y) { return x + y } } foo(3)(4)// 7 // 普通方法 var add = function(x, y) { return x + y; } add(3, 4)//7
本來應該一次傳入兩個引數的add函式,柯里化方法,變成每次呼叫都只用傳入一個引數,呼叫兩次後,得到最後的結果。
再看看,一道經典的面試題。
編寫一個sum函式,實現如下功能: console.log(sum(1)(2)(3)) // 6.
直接套用上面柯里化函式,多加一層return
function sum(a) { return function(b) { return function(c) { return a + b + c; } } }
當然,柯里化不是為了解決面試題,它是應函數語言程式設計而生,
如何實現
還是看看上面的經典面試題。
如果想實現sum(1)(2)(3)(4)(5)...(n)
就得巢狀n-1
個匿名函式,
function sum(a) { return function(b) { ... return function(n) { } } }
看起來並不優雅,如果我們預先知道有多少個引數要傳入,可以利用遞迴方法解決
var add = function(num1, num2) { return num1 + num2; } // 假設 sum 函式呼叫時,傳入引數都是標準的數字 function curry(add, n) { var count = 0, arr = []; return function reply(arg) { arr.push(arg); if ( ++count >= n) { //這裡也可以在外面定義變數,儲存每次計算後結果 return arr.reduce(function(p, c) { return p = add(p, c); }, 0) } else { return reply; } } } var sum = curry(add, 4); sum(4)(3)(2)(1)// 10
如果呼叫次數多於約定數量,sum
就會報錯,我們就可以設計成類似這樣
sum(1)(2)(3)(4)(); // 最後傳入空引數,標識呼叫結束,
只需要簡單修改下curry
函式
function curry(add) { var arr = []; return function reply() { var arg = Array.prototype.slice.call(arguments); arr = arr.concat(arg); if (arg.length === 0) { // 遞迴結束條件,修改為 傳入空引數 return arr.reduce(function(p, c) { return p = add(p, c); }, 0) } else { return reply; } } } console.log(sum(4)(3)(2)(1)(5)())// 15
簡潔版實現
上面針對具體問題,引入柯里化方法解答,回到如何實現建立柯里化函式的通用方法。
同樣先看簡單版本的方法,以add
方法為例,程式碼來自《JavaScript高階程式設計》
function curry(fn) { var args = Array.prototype.slice.call(arguments, 1); return function() { var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return fn.apply(null, finalArgs); }; } function add(num1, num2) { return num1 + num2; } var curriedAdd = curry(add, 5); var curriedAdd2 = curry(add, 5, 12); alert(curriedAdd(3))// 8 alert(curriedAdd2())// 17
加強版實現
上面add函式,可以換成任何其他函式,經過curry函式處理,都可以轉成柯里化函式。
這裡在呼叫curry初始化時,就傳入了一個引數,而且返回的函式curriedAdd
,curriedAdd2
也沒有被柯里化。要想實現更加通用的方法,在柯里化函式真正呼叫時,再傳引數,
function curry(fn) { ... } function add(num1, num2) { return num1 + num2; } var curriedAdd = curry(add); curriedAdd(3)(4) // 7
每次呼叫curry
返回的函式,也被柯里化,可以繼續傳入一個或多個引數進行呼叫,
跟上面sum(1)(2)(3)(4)
非常類似,利用遞迴就可以實現。關鍵是遞迴的出口,這裡不能是傳入一個空引數的呼叫, 而是原函式定義時,引數的總個數,柯里化函式呼叫時,滿足了原函式的總個數,就返回計算結果,否則,繼續返回柯里化函式
。
原函式的入參總個數,可以利用length
屬性獲得
function add(num1, num2) { return num1 + num2; } add.length // 2
結合上面的程式碼,
var curry = function(f) { var len = f.length; return function t() { var innerLength = arguments.length, args = Array.prototype.slice.call(arguments); if (innerLength >= len) {// 遞迴出口,f.length return f.apply(undefined, args) } else { return function() { var innerArgs = Array.prototype.slice.call(arguments), allArgs = args.concat(innerArgs); return t.apply(undefined, allArgs) } } } } // 測試一下 function add(num1, num2) { return num1 + num2; } var curriedAdd = curry(add); add(2)(3);//5 // 一個引數 function identity(value) { return value; } var curriedIdentify = curry(identify); curriedIdentify(4) // 4
到此,柯里化通用函式可以滿足大部分需求了。
在使用 apply 遞迴呼叫的時候,預設傳入 undefined, 在其它場景下,可能需要傳入 context, 繫結指定環境
實際開發,推薦使用lodash.curry , 具體實現,可以參考下curry原始碼
使用場景
講了這麼多curry函式的不同實現方法,那麼實現了通用方法後,在那些場景下可以使用,或者說使用柯里化函式是否可以真實的提高程式碼質量,下面總結一下使用場景
-
引數複用
在《JavaScript高階程式設計》中簡單版的curry函式中
var curriedAdd = curry(add, 5)
在後面,使用curriedAdd函式時,預設都複用了
5
,不需要重新傳入兩個引數 -
延遲執行
上面傳入多個引數的
sum(1)(2)(3)
,就是延遲執行的最後例子,傳入引數個數沒有滿足原函式入參個數,都不會立即返回結果。類似的場景,還有繫結事件回撥,更使用bind()方法繫結上下文,傳入引數類似,
addEventListener('click', hander.bind(this, arg1,arg2...)) addEventListener('click', curry(hander))
延遲執行的特性,可以避免在執行函式外面,包裹一層匿名函式,curry函式作為回撥函式就有很大優勢。
-
函數語言程式設計中,作為compose, functor, monad 等實現的基礎
有人說柯里化是應函數語言程式設計而生,它在裡面出現的概率就非常大了,在JS 函數語言程式設計指南 中,開篇就介紹了柯里化的重要性。
關於額外開銷
函式柯里化可以用來構建複雜的演算法 和 功能, 但是濫用 也會帶來額外的開銷。
從上面實現部分的程式碼中,可以看到,使用柯里化函式,離不開閉包, arguments, 遞迴。
閉包,函式中的變數都儲存在記憶體中,記憶體消耗大,有可能導致記憶體洩漏。
遞迴,效率非常差,
arguments, 變數存取慢,訪問性很差,