咱們常常說在Javascript語言中,函數是「一等公民」,它們本質上是十分簡單和過程化的。能夠利用函數,進行一些簡單的數據處理,return
結果,或者有一些額外的功能,須要經過使用閉包來實現,最後常常會return
匿名函數。html
若是你對函數式編程有必定了解,函數柯里化(function currying)是不可或缺的,利用函數柯里化,能夠在開發中很是優雅的處理複雜邏輯。git
柯里化(Currying),維基百科上的解釋是,把接受多個參數的函數轉換成接受一個單一參數的函數
先看一個簡單例子github
// 柯里化 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
npm
function sum(a) { return function(b) { return function(c) { return a + b + c; } } }
固然,柯里化不是爲了解決面試題,它是應函數式編程而生,編程
仍是看看上面的經典面試題。
若是想實現 sum(1)(2)(3)(4)(5)...(n)
就得嵌套n-1
個匿名函數,segmentfault
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
就會報錯,咱們就能夠設計成相似這樣app
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函數做爲回調函數就有很大優點。
有人說柯里化是應函數式編程而生,它在裏面出現的機率就很是大了,在JS 函數式編程指南中,開篇就介紹了柯里化的重要性。
函數柯里化能夠用來構建複雜的算法 和 功能, 可是濫用也會帶來額外的開銷。
從上面實現部分的代碼中,能夠看到,使用柯里化函數,離不開閉包, arguments, 遞歸。
閉包,函數中的變量都保存在內存中,內存消耗大,有可能致使內存泄漏。
遞歸,效率很是差,
arguments, 變量存取慢,訪問性不好,