柯里化(Curring, 以邏輯學家Haskell Curry命名)html
柯里化理解的基礎來源於咱們前幾篇文章構建的知識,若是還未能掌握閉包,建議回閱前文。前端
代碼例子會用到 apply/call
,通常用來實現對象冒充,例如字符串冒充數組對象,讓字符串擁有數組的方法。待對象講解篇會細分解析。在此先了解,二者功能相同,區別在於參數傳遞方式的不一樣, apply
參數以數組方式傳遞,call
多個參數則是逗號隔開。git
apply(context, [arguments]); call(context, arg1, arg2, arg3, ....);
代碼例子中使用到了ES6語法,對ES6還不熟悉的話,可學習社區這篇文章:《30分鐘掌握ES6/ES2015核心內容(上)》程序員
函數柯里化在JavaScript中實際上是高階函數的一種應用,上篇文章咱們簡略介紹了高階函數(能夠做爲參數傳遞,或做爲返回值)。github
理論知識太枯燥,來個生活小例子,"存款買房"(富二代繞道)。假設買房是咱們存錢的終極目標。那麼在買房前,存在卡里的錢(老婆本)就不能動。等到夠錢買房了,錢從銀行卡取出來,開始買買買。。。面試
函數柯里化就像咱們往卡里存錢,存夠了,才能執行買房操做,存不夠,接着存。編程
先上幾個公式(左邊是普通函數,右邊就是轉化後柯里化函數支持的調用方式):segmentfault
// 公式類型一 fn(a,b,c,d) => fn(a)(b)(c)(d); fn(a,b,c,d) => fn(a, b)(c)(d); fn(a,b,c,d) => fn(a)(b,c,d); // 公式類型二 fn(a,b,c,d) => fn(a)(b)(c)(d)(); fn(a,b,c,d) => fn(a);fn(b);fn(c);fn(d);fn();
兩種公式類型的區別 —— 函數觸發執行的機制不一樣:數組
經過公式,咱們先來理解這行代碼 fn(a)(b)(c)(d)
, 執行 fn(a)
時返回的是一個函數,而且支持傳參。什麼時候返回的是值而不是函數的觸發機制控制權在咱們手裏,咱們能夠爲函數制定不一樣的觸發機制。瀏覽器
普通的函數調用,一次性傳入參數就執行。而經過柯里化,它能夠幫咱們實現函數部分參數傳入執行(並未當即執行原始函數,錢沒存夠接着存),這就是函數柯里化的特色:"延遲執行和部分求值"
"函數柯里化:指封裝一個函數,接收原始函數做爲參數傳入,並返回一個可以接收並處理剩餘參數的函數"
// 等待咱們柯里化實現的方法add function add(a, b, c, d) { return a + b + c + d; };
// 最簡單地實現函數add的柯里化 // 有點low,有助於理解 function add(a, b, c, d) { return function(a) { return function(b) { return function(c) { return a + b + c + d; } } } }
分析代碼知識點:
前幾篇文章的知識點此時恰好。可見基礎知識的重要性,高階的東西始終要靠小磚頭堆砌出來。
弄清原理後,接下來就是將代碼寫得更通用些(高大上些)。
// 公式類型一: 參數數量知足函數參數要求,觸發執行 // fn(a,b,c,d) => fn(a)(b)(c)(d); const createCurry = (fn, ...args) => { let _args = args || []; let length = fn.length; // fn.length代碼函數參數數量 return (...rest) => { let _allArgs = _args.slice(0); // 深拷貝閉包共用對象_args,避免後續操做影響(引用類型) _allArgs.push(...rest); if (_allArgs.length < length) { // 參數數量不知足原始函數數量,返回curry函數 return createCurry.call(this, fn, ..._allArgs); } else { // 參數數量知足原始函數數量,觸發執行 return fn.apply(this, _allArgs); } } } const curryAdd = createCurry(add, 2); let sum = curryAdd(3)(4)(5); // 14 // ES5寫法 function createCurry() { var fn = arguments[0]; var _args = [].slice.call(arguments, 1); var length = fn.length; return function() { var _allArgs = _args.slice(0); _allArgs = _allArgs.concat([].slice.call(arguments)); if (_allArgs.length < length) { _allArgs.unshift(fn); return createCurry.apply(this, _allArgs); } else { return fn.apply(this, _allArgs); } } }
// 公式類型二: 無參數傳入時而且參數數量已經知足函數要求 // fn(a, b, c, d) => fn(a)(b)(c)(d)(); // fn(a, b, c, d) => fn(a);fn(b);fn(c);fn(d);fn(); const createCurry = (fn, ...args) => { let all = args || []; let length = fn.length; return (...rest) => { let _allArgs = all.slice(0); _allArgs.push(...rest); if (rest.length > 0 || _allArgs.length < length) { // 調用時參數不爲空或存儲的參數不知足原始函數參數數量時,返回curry函數 return createCurry.call(this, fn, ..._allArgs); } else { // 調用參數爲空(),且參數數量知足時,觸發執行 return fn.apply(this, _allArgs); } } } const curryAdd = createCurry(2); let sum = curryAdd(3)(4)(5)(); // 14 // ES5寫法 function createCurry() { var fn = arguments[0]; var _args = [].slice.call(arguments, 1); var length = fn.length; return function() { var _allArgs = _args.slice(0); _allArgs = _allArgs.concat([].slice.call(arguments)); if (arguments.length > 0 || _allArgs.length < length) { _allArgs.unshift(fn); return createCurry.apply(this, _allArgs); } else { return fn.apply(this, _allArgs); } } }
爲實現公式中不一樣的兩種調用公式,兩個createCurry方法制定了兩種不一樣的觸發機制。記住一個點,函數觸發機制可根據需求自行制定。
先上個公式看對比:
// 函數柯里化:參數數量完整 fn(a,b,c,d) => fn(a)(b)(c)(d); fn(a,b,c,d) => fn(a,b)(c)(d); // 偏函數:只執行了部分參數 fn(a,b,c,d) => fn(a); fn(a,b,c,d) => fn(a, b);
"函數柯里化中,當你傳入部分參數時,返回的並非原始函數的執行結果,而是一個能夠繼續支持後續參數的函數。而偏函數的調用方式更像是普通函數的調用方式,只調用一次,它經過原始函數內部來實現不定參數的支持。"
若是已經看懂上述柯里化的代碼例子,那麼改寫支持偏函數的代碼,並不難。
// 公式: // fn(a, b, c, d) => fn(a); // fn(a, b, c, d) => fn(a,b,c); const partialAdd = (a = 0, b = 0, c = 0, d = 0) => { return a + b + c +d; } partialAdd(6); // 6 partialAdd(2, 3); // 5
使用ES6函數參數默認值,爲沒有傳入參數,指定默認值爲0,支持無參數或不定參數傳入。
柯里化是犧牲了部分性能來實現的,可能帶來的性能損耗:
arguments
對象要比存取命名參數要慢一些arguments.lengths
的實現至關慢(新版本瀏覽器忽略)fn.apply()
和 fn.call()
要比直接調用 fn()
慢// 普通事件綁定函數 var addEvent = function(ele, type, fn, isCapture) { if(window.addEventListener) { ele.addEventListener(type, fn, isCapture) } else if(window.attachEvent) { ele.attachEvent("on" + type, fn) } } // 弊端:每次調用addEvent都會進行判斷 // 柯里化事件綁定函數 var addEvent = (function() { if(window.addEventListener) { return function(ele, type, fn, isCapture) { ele.addEventListener(type, fn, isCapture) } } else if(window.attachEvent) { return function(ele, type, fn) { ele.attachEvent("on" + type, fn) } } })() // 優點:判斷只執行一次,經過閉包保留了父級做用域的判斷結果
先上公式,歷來沒有這麼喜歡寫公式,簡明易懂。
// 反柯里化公式: curryFn(a)(b)(c)(d) = fn(a, b, c, d); curryFn(a) = fn(a);
看完公式,是否是似曾相識,這不就是咱們平常敲碼的普通函數麼?沒錯的,函數柯里化就是把普通函數變成成一個複雜的函數,而反柯里化其就是柯里化的逆反,把複雜變得簡單。
函數柯里化是把支持多個參數的函數變成接收單一參數的函數,並返回一個函數能接收處理剩餘參數:fn(a,b,c,d) => fn(a)(b)(c)(d)
,而反柯里化就是把參數所有釋放出來:fn(a)(b)(c)(d) => fn(a,b,c,d)
。
// 反柯里化:最簡單的反柯里化(普通函數) function add(a, b, c, d) { return a + b + c + d; }
函數柯里化是函數編程中的一個重要的基礎,它爲咱們提供了一種編程的思惟方式。顯然,它讓咱們的函數處理變得複雜,代碼調用方式並不直觀,還加入了閉包,多層做用域嵌套,會有一些性能上的影響。
但在一些複雜的業務邏輯封裝中,函數柯里化可以爲咱們提供更好的應對方案,讓咱們的函數更具自由度和靈活性。
實際開發中,若是你的邏輯處理相對複雜,不妨換個思惟,用函數柯里化來實現,技能包不嫌多。
說到底,程序員就是解決問題的那羣人。
本篇函數柯里化知識點的理解確實存在難度,暫時跳過這章也無妨,能夠先了解再深刻。耐得主寂寞的小夥伴回頭多啃幾遍,沒準春季面試就遇到了。
參考文檔:
本文首發Github,期待Star!
https://github.com/ZengLingYong/blog
做者:以樂之名 本文原創,有不當的地方歡迎指出。轉載請指明出處。