應該不少童鞋都聽過函數式編程(Functional programming)的概念吧,可能有的童鞋有據說過函數式編程但並非特別瞭解,但其實在咱們的開發過程當中,或多或少都已經應用了函數式編程的思想。編程
相對於面向對象編程(Object-oriented programming)關注的是數據而言,函數式編程關注的則是動做,其是一種過程抽象的思惟,就是對當前的動做去進行抽象。數組
好比說我要計算一個數 加上 4 再乘以 4 的值,按照正常寫代碼的邏輯,咱們可能會這麼去實現bash
function calculate(x){
return (x + 4) * 4;
}
console.log(calculate(1)) // 20
複製代碼
這是沒有任何問題的,咱們在平時開發的過程當中會常常將須要重複的操做封裝成函數以便在不一樣的地方可以調用。但從函數式編程的思惟來看的話,咱們關注的則是這一系列操做的動做,先「加上 4」再「乘以 4」。數據結構
如何封裝函數纔是最佳實踐呢?如何封裝才能使函數更加通用,使用起來讓人感受更加舒服呢?函數式編程或許能給咱們一些啓發。app
函數式編程具備兩個基本特徵。dom
第一等公民是指函數跟其它的數據類型同樣處於平等地位,能夠賦值給其餘變量,能夠做爲參數傳入另外一個函數,也能夠做爲別的函數的返回值。函數式編程
// 賦值
var a = function fn1() { }
// 函數做爲參數
function fn2(fn) {
fn()
}
// 函數做爲返回值
function fn3() {
return function() {}
}
複製代碼
純函數是指相同的輸入總會獲得相同的輸出,而且不會產生反作用的函數。函數
從純函數的概念咱們能夠知道純函數具備兩個特色:優化
無反作用指的是函數內部的操做不會對外部產生影響(如修改全局變量的值、修改 dom 節點等)。ui
// 是純函數
function add(x,y){
return x + y
}
// 輸出不肯定,不是純函數
function random(x){
return Math.random() * x
}
// 有反作用,不是純函數
function setColor(el,color){
el.style.color = color ;
}
// 輸出不肯定、有反作用,不是純函數
var count = 0;
function addCount(x){
count+=x;
return count;
}
複製代碼
函數式編程具備兩個最基本的運算:合成(compose)和柯里化(Currying)。
函數合成指的是將表明各個動做的多個函數合併成一個函數。
上面講到,函數式編程是對過程的抽象,關注的是動做。以上面計算的例子爲例,咱們關注的是它的動做,先「加上 4」再「乘以 4」。那麼咱們的代碼實現以下
function add4(x) {
return x + 4
}
function multiply4(x) {
return x * 4
}
console.log(multiply4(add4(1))) // 20
複製代碼
根據函數合成的定義,咱們可以將上述表明兩個動做的兩個函數的合成一個函數。咱們將合成的動做抽象爲一個函數 compose,這裏能夠比較容易地知道,函數 compose 的代碼以下
function compose(f,g) {
return function(x) {
return f(g(x));
};
}
複製代碼
因此咱們能夠經過以下的方式獲得合成函數
var calculate=compose(multiply4,add4); //執行動做的順序是從右往左
console.log(calculate(1)) // 20
複製代碼
能夠看到,只要往 compose 函數中傳入表明各個動做的函數,咱們便能獲得最終的合成函數。但上述 compose 函數的侷限性是隻可以合成兩個函數,若是須要合成的函數不止兩個呢,因此咱們須要一個通用的 compose 函數。
這裏我直接給出通用 compose 函數的代碼
function compose() {
var args = arguments;
var start = args.length - 1;
return function () {
var i = start - 1;
var result = args[start].apply(this, arguments);
while (i >= 0){
result = args[i].call(this, result);
i--;
}
return result;
};
}
複製代碼
讓咱們來實踐下上述通用的 compose 函數~
function addHello(str){
return 'hello '+str;
}
function toUpperCase(str) {
return str.toUpperCase();
}
function reverse(str){
return str.split('').reverse().join('');
}
var composeFn=compose(reverse,toUpperCase,addHello);
console.log(composeFn('ttsy')); // YSTT OLLEH
複製代碼
上述過程有三個動做,「hello」、「轉換大寫」、「反轉」,能夠看到經過 compose 將上述三個動做表明的函數合併成了一個,最終輸出了正確的結果。
在維基百科中對柯里化的定義是:在計算機科學中,柯里化,又譯爲卡瑞化或加里化,是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數並且返回結果的新函數的技術。
柯里化函數則是將函數柯里化以後獲得的一個新函數。由上述定義可知,柯里化函數有以下兩個特性:
舉個例子~
function add(a, b) {
return a + b;
}
console.log(add(1, 2)) // 3
複製代碼
假設函數 add 的柯里化函數是 addCurry,那麼從上述定義可知,addCurry(1)(2) 應該實現與上述代碼相同的效果,輸出 3 。這裏咱們能夠比較容易的知道,addCurry 的代碼以下
// addCurry 是 add 的柯里化函數
function addCurry(a) {
return function(b) {
return a + b;
}
}
console.log(addCurry(1)(2)); // 3
複製代碼
但假設若是有一個函數 createCurry 可以實現柯里化,那麼咱們即可以經過下述的方式來得出相同的結果
// createCurry 返回一個柯里化函數
var addCurry=createCurry(add);
console.log(addCurry(1)(2)); // 3
複製代碼
能夠看到,函數 createCurry 傳入一個函數 add 做爲參數,返回一個柯里化函數 addCurry,函數 addCurry 可以處理 add 中的剩餘參數。這個過程稱爲函數柯里化,咱們稱 addCurry 是 add 的柯里化函數。
那麼,怎麼獲得實現柯里化的函數 createCurry 呢?這裏我直接給出 createCurry 的代碼
// 參數只能從左到右傳遞
function createCurry(func, arrArgs) {
var args=arguments;
var funcLength = func.length;
var arrArgs = arrArgs || [];
return function(param) {
var allArrArgs=arrArgs.concat([param])
// 若是參數個數小於最初的func.length,則遞歸調用,繼續收集參數
if (allArrArgs.length < funcLength) {
return args.callee.call(this, func, allArrArgs);
}
// 參數收集完畢,則執行func
return func.apply(this, allArrArgs);
}
}
複製代碼
咱們能夠經過以下方式去調用
// createCurry 返回一個柯里化函數
var addCurry=createCurry(function(a, b, c) {
return a + b + c;
});
console.log(addCurry(1)(2)(3)); // 6
複製代碼
上述 createCurry 函數已經可以實現柯里化的過程,可是其並無那麼完美,若是我但願以 addCurry(1, 2)(3) 的方式來調用呢?則上述代碼並不能給出咱們想要的結果,因此咱們要對 createCurry 作一個優化,優化後的 createCurry 代碼以下
// 參數只能從左到右傳遞
function createCurry(func, arrArgs) {
var args=arguments;
var funcLength = func.length;
var arrArgs = arrArgs || [];
return function() {
var _arrArgs = Array.prototype.slice.call(arguments);
var allArrArgs=arrArgs.concat(_arrArgs)
// 若是參數個數小於最初的func.length,則遞歸調用,繼續收集參數
if (allArrArgs.length < funcLength) {
return args.callee.call(this, func, allArrArgs);
}
// 參數收集完畢,則執行func
return func.apply(this, allArrArgs);
}
}
複製代碼
優化以後的 createCurry 函數則顯得更增強大
// createCurry 返回一個柯里化函數
var addCurry=createCurry(function(a, b, c) {
return a + b + c;
});
console.log(addCurry(1)(2)(3)); // 6
console.log(addCurry(1, 2, 3)); // 6
console.log(addCurry(1, 2)(3)); // 6
console.log(addCurry(1)(2, 3)); // 6
複製代碼
柯里化其實是把簡答的問題複雜化了,可是複雜化的同時,咱們在使用函數時擁有了更加多的自由度。
那麼,柯里化有什麼用途呢?舉個例子~
如今咱們須要實現一個功能,將一個全是數字的數組中的數字轉換成百分數的形式。按照正常的邏輯,咱們能夠按以下代碼實現
function getNewArray(array) {
return array.map(function(item) {
return item * 100 + '%'
})
}
console.log(getNewArray([1, 0.2, 3, 0.4])); // ['100%', '20%', '300%', '40%']
複製代碼
而若是經過柯里化的方式來實現
function map(func, array) {
return array.map(func);
}
var mapCurry = createCurry(map);
var getNewArray = mapCurry(function(item) {
return item * 100 + '%'
})
console.log(getNewArray([1, 0.2, 3, 0.4])); // ['100%', '20%', '300%', '40%']
複製代碼
上述例子可能太簡單以至不能表現出柯里化的強大,具體柯里化的使用還須要結合具體的場景,我的以爲沒有必要爲了柯里化而柯里化,咱們最終的目的是爲了更好地解決問題,不是麼?
在函數式編程中,還有一個很重要的概念是函子。
在前面函數合成的例子中,執行了先「加上 4」再「乘以 4」的動做,咱們能夠看到代碼中是經過 multiply4(add4(1)) 這種形式來實現的,若是經過 compose 函數,則是相似於 compose(multiply4,add4)(1) 這種形式來實現代碼。
而在函數式編程的思惟中,除了將動做抽象出來外,還但願動做執行的順序更加清晰,因此對於上面的例子來講,更但願是經過以下的形式來執行咱們的動做
fn(1).add4().multiply4()
複製代碼
這時咱們須要用到函子的概念。
function Functor(val){
this.val = val;
}
Functor.prototype.map=function(f){
return new Functor(f(this.val));
}
複製代碼
函子能夠簡單地理解爲有用到 map 方法的數據結構。如上 Functor 的實例就是一個函子。
在函子的 map 方法中接受一個函數參數,而後返回一個新的函子,新的函子中包含的值是被函數參數處理事後返回的值。該方法將函子裏面的每個值,映射到另外一個函子。
經過 Functor 函子,咱們能夠經過以下的方式調用
console.log((new Functor(1)).map(add4).map(multiply4)) // Functor { val: 20 }
複製代碼
上述調用的方式是 (new Calculate(1)).map(add4).map(multiply4) ,跟咱們想要的效果已經差很少了,可是咱們不但願有 new 的存在,因此咱們在 Functor 函子掛載上 of 方法
function Functor(val){
this.val = val;
}
Functor.prototype.map=function(f){
return new Functor(f(this.val));
}
Functor.of = function(val) {
return new Functor(val);
}
複製代碼
最終咱們能夠經過以下方式調用
console.log(Functor.of(1).map(add4).map(multiply4)) // Functor { val: 20 }
複製代碼
接下來介紹各類常見的函子。
Maybe 函子是指在 map 方法中增長了對空值的判斷的函子。
因爲函子中的 map 方法中的函數參數會對函子內部的值進行處理,因此當傳入函子中的值爲空(如 null)時,則可能會產生錯誤。
function toUpperCase(str) {
return str.toUpperCase();
}
console.log(Functor.of(null).map(toUpperCase)); // TypeError
複製代碼
Maybe 函子則在 map 方法中增長了對空值的判斷,如果函子內部的值爲空,則直接返回一個內部值爲空的函子。
function Maybe(val){
this.val = val;
}
Maybe.prototype.map=function(f){
return this.val ? Maybe.of(f(this.val)) : Maybe.of(null);
}
Maybe.of = function(val) {
return new Maybe(val);
}
複製代碼
當使用 Maybe 函子時傳入空值則不會報錯
console.log(Maybe.of(null).map(toUpperCase)); // Maybe { val: null }
複製代碼
Either 函子是指內部有分別有左值(left)和右值(right),正常狀況下會使用右值,而當右值不存在的時候會使用左值的函子。
function Either(left,right){
this.left = left;
this.right = right;
}
Either.prototype.map=function(f){
return this.right ? Either.of(this.left, f(this.right)) : Either.of(f(this.left), this.right);
}
Either.of = function(left,right) {
return new Either(left,right);
}
複製代碼
以下當左右值都存在的時候則以右值爲函子的默認值,當右值不存在是則以左值爲函子的默認值。
function addOne(x) {
return x+1;
}
console.log(Either.of(1,2).map(addOne)); // Either { left: 1, right: 3 }
console.log(Either.of(3,null).map(addOne)); // Either { left: 4, right: null }
複製代碼
Monad 函子是指可以將函子多層嵌套解除的函子。
咱們往函子傳入的值不只僅能夠是普通的數據類型,也能夠是其它函子,當往函子內部傳其它函子的時候,則會出現函子的多層嵌套。以下
var functor = Functor.of(Functor.of(Functor.of('ttsy')))
console.log(functor); // Functor { val: Functor { val: Functor { val: 'ttsy' } } }
console.log(functor.val); // Functor { val: Functor { val: 'ttsy' } }
console.log(functor.val.val); // Functor { val: 'ttsy' }
複製代碼
Monad 函子中新增了 join 和 flatMap 方法,經過 flatMap 咱們可以在每一次傳入函子的時候都將嵌套解除。
Monad.prototype.map=function(f){
return Monad.of(f(this.val))
}
Monad.prototype.join=function(){
return this.val;
}
Monad.prototype.flatMap=function(f){
return this.map(f).join();
}
Monad.of = function(val) {
return new Monad(val);
}
複製代碼
經過 Monad 函子,咱們最終獲得的都是隻有一層的函子。
console.log(Monad.of('ttsy').flatMap(Monad.of).flatMap(Monad.of)); // Monad { val: 'TTSY' }
複製代碼
在咱們平時的開發過程當中,要根據不一樣的場景去實現不一樣功能的函數,而函數式編程則讓咱們從不一樣的角度去讓咱們可以以最佳的方式去實現函數功能,但函數式編程不是非此即彼的,而是要根據不一樣的應用場景去選擇不一樣的實現方式。
以爲還不錯的小夥伴,能夠關注一波公衆號哦。