js函數柯里化

        因爲最近離職找工做的事情,很久不寫文章了,就只好把2017年寫的文章搬出來騙騙讚了hhh。react

概念

       用我本身的話來總結一下,函數柯里化的意思就是你能夠一次傳不少參數給curry函數,也能夠分屢次傳遞,curry函數每次都會返回一個函數去處理剩下的參數,一直到返回最後的結果。正則表達式

實例

        這裏仍是舉幾個例子來講明一下:redux

柯里化求和函數

// 普通方式
    var add1 = function(a, b, c){
        return a + b + c;
    }
    // 柯里化
    var add2 = function(a) {
        return function(b) {
            return function(c) {
                return a + b + c;
            }
        }
    }
複製代碼

        這裏每次傳入參數都會返回一個新的函數,這樣一直執行到最後一次返回a+b+c的值。         可是這種實現仍是有問題的,這裏只有三個參數,若是哪天產品經理告訴咱們須要改爲100次?咱們就從新寫100次?這很明顯不符合開閉原則,因此咱們須要對函數進行一次修改。瀏覽器

var add = function() {
    var _args = [];
    return function() {
        if(arguments.length === 0) {
            return _args.reduce(function(a, b) {
                return a + b;
            })
        }
        [].push.apply(_args, arguments);
        return arguments.callee;
    }
}
var sum = add();
sum(100, 200)(300);
sum(400);
sum(); // 1000
複製代碼

        咱們經過判斷下一次是否傳進來參數來決定函數是否運行,若是繼續傳進了參數,那咱們繼續把參數都保存起來,等運行的時候所有一次性運行,這樣咱們就初步完成了一個柯里化的函數。bash

通用柯里化函數

        這裏只是一個求和的函數,若是換成求乘積呢?咱們是否是又須要從新寫一遍?仔細觀察一下咱們的add函數,若是咱們將if裏面的代碼換成一個函數執行代碼,是否是就能夠變成一個通用函數了?閉包

var curry = function(fn) {
    var _args = [];
    return function() {
        if(arguments.length === 0) {
            return fn.apply(fn, _args);
        }
        [].push.apply(_args, arguments);
        return arguments.callee;
    }
}
var multi = function() {
    return [].reduce.call(arguments, function(a, b) {
        return a + b;
    })
}
var add = curry(multi);
add(100, 200, 300)(400);
add(1000);
add(); // 2000

複製代碼

        在以前的方法上面,咱們進行了擴展,這樣咱們就已經實現了一個比較通用的柯里化函數了。         也許你想問,我不想每次都使用那個醜陋的括號結尾怎麼辦?app

var curry = function(fn) {
	var len = fn.length,
		args = [];
	return function() {
		Array.prototype.push.apply(args, arguments)
		var argsLen = args.length;
		if(argsLen < len) {
			return arguments.callee;
		}
		return fn.apply(fn, args);
	}
}
var add = function(a, b, c) {
	return a + b + c;
}

var adder = curry(add)
adder(1)(2)(3)
複製代碼

        這裏根據函數fn的參數數量進行判斷,直到傳入的數量等於fn函數須要的參數數量纔會返回fn函數的最終運行結果,和上面那種方法原理實際上是同樣的,可是這兩種方式都太依賴參數數量了。         我在簡書還看到別人的另外一種遞歸實現方法,其實實現思路和個人差很少吧。less

// 簡單實現,參數只能從右到左傳遞
function createCurry(func, args) {

    var arity = func.length;
    var args = args || [];

    return function() {
        var _args = [].slice.call(arguments);
        [].push.apply(_args, args);

        // 若是參數個數小於最初的func.length,則遞歸調用,繼續收集參數
        if (_args.length < arity) {
            return createCurry.call(this, func, _args);
        }

        // 參數收集完畢,則執行func
        return func.apply(this, _args);
    }
}
複製代碼

        這裏是對參數個數進行了計算,若是須要無限參數怎麼辦?好比下面這種場景。函數

add(1)(2)(3)(2);
add(1, 2, 3, 4, 5);
複製代碼

       這裏主要有一個知識點,那就是函數的隱式轉換,涉及到toString和valueOf兩個方法,若是直接對函數進行計算,那麼會先把函數轉換爲字符串,以後再參與到計算中,利用這兩個方法咱們能夠對函數進行修改。ui

var num = function() {
}
num.toString = num.valueOf = function() {
	return 10;
}
var anonymousNum = (function() { // 10
	return num;
}())
複製代碼

        通過修改,咱們的函數最終版是這樣的。

var curry = function(fn) {
	var func = function() {
		var _args = [].slice.call(arguments, 0);
		var func1 = function() {
			[].push.apply(_args, arguments)
			return func1;
		}
		func1.toString = func1.valueOf = function() {
			return fn.apply(fn, _args);
		}
		return func1;
	}
	return func;
}
var add = function() {
	return [].reduce.call(arguments, function(a, b) {
		return a + b;
	})
}

var adder = curry(add)
adder(1)(2)(3)
複製代碼

        那麼咱們說了那麼多,柯里化究竟有什麼用呢?

預加載

        在不少場景下,咱們須要的函數參數極可能有一部分同樣,這個時候再重複寫就比較浪費了,咱們提早加載好一部分參數,再傳入剩下的參數,這裏主要是利用了閉包的特性,經過閉包能夠保持着原有的做用域。

var match = curry(function(what, str) {
  return str.match(what);
});

match(/\s+/g, "hello world");
// [ ' ' ]

match(/\s+/g)("hello world");
// [ ' ' ]

var hasSpaces = match(/\s+/g);
// function(x) { return x.match(/\s+/g) }

hasSpaces("hello world");
// [ ' ' ]

hasSpaces("spaceless");
// null
複製代碼

        上面例子中,使用hasSpaces函數來保存正則表達式規則,這樣能夠有效的實現參數的複用。

動態建立函數

        這個其實也是一種惰性函數的思想,咱們能夠提早執行判斷條件,經過閉包將其保存在有效的做用域中,來看一種咱們平時寫代碼常見的場景。

var addEvent = function(el, type, fn, capture) {
     if (window.addEventListener) {
         el.addEventListener(type, function(e) {
             fn.call(el, e);
         }, capture);
     } else if (window.attachEvent) {
         el.attachEvent("on" + type, function(e) {
             fn.call(el, e);
         });
     } 
 };
複製代碼

        在這個例子中,咱們每次調用addEvent的時候都會從新進行if語句進行判斷,可是實際上瀏覽器的條件不可能會變化,你判斷一次和判斷N次結果都是同樣的,因此這個能夠將判斷條件提早加載。

var addEventHandler = function(){
    if (window.addEventListener) {
        return function(el, sType, fn, capture) {
            el.addEventListener(sType, function(e) {
                fn.call(el, e);
            }, (capture));
        };
    } else if (window.attachEvent) {
        return function(el, sType, fn, capture) {
            el.attachEvent("on" + sType, function(e) {
                fn.call(el, e);
            });
        };
    }
}
var addEvent = addEventHandler();
addEvent(document.body, "click", function() {}, false);
addEvent(document.getElementById("test"), "click", function() {}, false);
複製代碼

        可是這樣作仍是有一種缺點,由於咱們沒法判斷程序中是否使用了這個方法,可是依然不得不在文件頂部定義一下addEvent,這樣其實浪費了資源,這裏有一種更好的解決方法。

var addEvent = function(el, sType, fn, capture){
    if (window.addEventListener) {
        addEvent =  function(el, sType, fn, capture) {
            el.addEventListener(sType, function(e) {
                fn.call(el, e);
            }, (capture));
        };
    } else if (window.attachEvent) {
        addEvent = function(el, sType, fn, capture) {
            el.attachEvent("on" + sType, function(e) {
                fn.call(el, e);
            });
        };
    }
}
複製代碼

        在addEvent函數裏面對其從新賦值,這樣既解決了每次運行都要判斷的問題,又解決了必須在做用域頂部執行一次形成浪費的問題。

React

        在回家的路上我一直在想函數柯里化是否是能夠擴展到更多場景,我想把函數換成react組件試試?我想到了高階組件和redux的connect,這兩個確實是將柯里化思想用到react裏面的體現。咱們想想,若是把上面例子裏面的函數換成組件,參數換成高階函數呢?

var curry = function(fn) {
	var func = function() {
		var _args = [].slice.call(arguments, 0);
		var func1 = function() {
			[].push.apply(_args, arguments)
			return func1;
		}
		func1.toString = func1.valueOf = function() {
			return fn.apply(fn, _args);
		}
		return func1;
	}
	return func;
}

var hoc = function(WrappedComponent) {
	return function() {
		var len = arguments.length;
		var NewComponent = WrappedComponent;
		for (var i = 0; i < len; i++) {
			NewComponent = arguments[i](NewComponent)
		}
		return NewComponent;
	}
}
var MyComponent = hoc(PageList);
curry(MyComponent)(addStyle)(addLoading)
複製代碼

這個例子是對原來的PageList組件進行了擴展,給PageList加了樣式和loading的功能,若是想加其餘功能,能夠繼續在上面擴展(注意addStyle和addLoading都是高階組件),可是寫法真的很糟糕,一點都不coooooool,咱們可使用compose方法,underscore和loadsh這些庫中已經提供了。

var enhance = compose(addLoading, addStyle);
enhance(MyComponent)
複製代碼

總結

        其實關於柯里化的運用核心仍是對函數閉包的靈活運用,深入理解閉包和做用域後就能夠寫出不少靈活巧妙的方法。

相關文章
相關標籤/搜索