深刻理解JavaScript函數式編程

函數式編程的思惟方式是把現實世界的事物和事物之間的聯繫抽象到程序世界(對運算過程進行抽象). (本篇文章內容輸出來源:《拉鉤教育大前端訓練營》部分參考書籍:《JavaScript忍者祕籍》《你不知道的JavaScript 卷一》關於函數部分的講解 進行總結)javascript

本章重點掌握Javascript中的高階函數知識以及函數式編程.html

爲何要學習函數式編程?前端

  • vue/react 開始擁抱函數式編程
  • 函數式編程能夠拋棄this
  • 打包過程當中能夠更好的利用tree shaking過濾無用的代碼
  • 方便測試、方便並行處理
  • 有不少庫能夠幫助咱們進行函數式開發:loadsh、underscore、ramda

什麼是函數式編程

什麼是函數式編程(Functional Programming, FP):FP 是編程範式之一.(還有面向過程編程、面向對象編程)vue

面向對象編程的思惟方式: 把現實世界中的事物抽象成程序世界中的類和對象,經過封裝、繼承和多態來演示事物事件的聯繫java

函數式編程的思惟方式是把現實世界的事物和事物之間的聯繫抽象到程序世界(對運算過程進行抽象).node

  • 程序的本質:根據輸入經過某種運算得到相應的輸出,程序開發過程當中會涉及不少有輸入和輸出的函數
  • x ->f(聯繫、映射)->y,y=f(x)
  • 函數式編程中的函數指的不是程序中的函數(方法),而是數學中的函數即映射關係,例如:​y=sin(x),x和y的關係
  • 相同的輸入始終要獲得相同的輸出
  • 函數式編程用來描述數據(函數)之間的映射
function test(x){
	return x * x;
}
複製代碼

在Javascript中函數是一等公民,函數能夠存儲在變量中、函數做爲參數、函數能夠做爲返回值.react

JavaScript中的高階函數

高階函數程序員

函數做爲參數,以下代碼實現的是循環遍歷數組,經過傳遞參數回調函數能夠拿到每一個數組遍歷的值在回調函數中進行相應的處理web

//模擬forEach
function forEach(array, fn) {
    for (let index = 0; index < array.length; index++) {
        const element = array[index];
        fn(element);
    }
}
複製代碼

函數做爲返回值,以下函數能夠做爲返回值,以下代碼通常來講函數做爲返回值是閉包的表現,關於閉包的概念會在後面詳細的學習數據庫

function test(x){
	return function(y){
				return x + y;
	}
}
let a = test(1)(2);//3
複製代碼

高階函數的意義

  • 抽象幫助咱們屏蔽細節,只須要關注咱們的目標
  • 高階函數是用來抽象通用的問題

面向過程方式與函數式編程方式對比

經常使用高階函數,下面來模擬JavaScript中的自帶的高階函數,以下代碼經常使用的高階函數大量都使用了以函數做爲參數,進行回調。只須要拿到結果進行處理便可。

  • forEach - 函數做爲參數
//模擬forEach
function forEach(array, fn) {
    for (let index = 0; index < array.length; index++) {
        const element = array[index];
        fn(element);
    }
}
複製代碼
  • filter - 函數做爲參數
//模擬filter
function filter(array, fn) {
    let result = [];
    for (let index = 0; index < array.length; index++) {
        const element = array[index];
        if (fn(element)) {
            result.push(element);
        }
    }
    return result;
}
複製代碼
  • every-函數做爲參數
//every 數組的全部元素進行某種操做所有爲真匹配條件才返回真 不然只要有一個不成立就會返回false假
const every = (arr, fn) => {
    let result = false;
    for (const iterator of arr) {
        result = fn(iterator);
        //只要有一個返回爲false就不成立
        if (!result) {
            break;
        }
    }
    return result;
}
複製代碼
  • some-函數做爲參數
//模擬some函數 數組中的元素只要有一個元素匹配條件返回爲true,只有全部元素所有不匹配條件纔會返回false
const some = (arr, fn) => {
    let result = false;
    for (const value of arr) {
        result = fn(value);
        if (result) {
            break;
        }
    }
    return result;
}
複製代碼
  • once - 函數做爲參數
//模擬once函數 只能執行一次
function once(fn) {
    let done = false;
    return function () {
        if (!done) {
            done = true;
            return fn.apply(this, arguments);//調用function() 傳遞的參數 傳遞到fn
        }
    }
}

let pay = once((money) => {
    console.log(`支付了${money} RMB`);
});
複製代碼
  • map - 函數做爲參數
//模擬map函數 對數組中對每個元素遍歷改變每個元素的值 使用const 不但願函數被修改定義爲常量
const map = (array, fn) => {
    let results = [];
    for (const value of array) {
        results.push(fn(value));//獲得的是fn的處理的結果
    }
    return results;
}
複製代碼

閉包

閉包:函數和其周圍的狀態(詞法環境)的引用捆綁在一塊兒造成閉包.

  • 閉包能夠在另外一個做用域中調用一個函數的內部函數並訪問到該函數的做用域中的成員

如上述的once函數,返回的新的函數依然能夠調用once()函數中的內部變量done

function once(fn) {
    let done = false;
    return function () {
        if (!done) {
            done = true;
            return fn.apply(this, arguments);//調用function() 傳遞的參數 傳遞到fn
        }
    }
}
複製代碼
  • 閉包的本質:函數在執行的時候會放到一個執行棧上當函數執行完畢以後會從執行棧上移除,可是堆上的做用域成員由於被外部引用不能釋放,所以內部函數依然能夠訪問外部函數的成員.

閉包的深刻理解

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script> /* 閉包的案例 */ Math.pow(4,2);//4的二次方 5的二次方 //經過一個函數來簡化求平方 function makePow(power){ //返回一個函數求傳遞的數的power次冪 return function(value){ return Math.pow(value,power); } } //求平方 let power2 = makePow(2); //求三次方 let power3 = makePow(3); console.log(power2(2)); console.log(power2(4)); console.log(power3(4)); </script>
</body>
</html>
複製代碼

下面咱們經過調試上述的代碼,來看一下閉包的過程

以下圖,重點關注的有兩個地方,一個設置調試點而後刷新頁面能夠看到右側的調試工具,重點關注右側的Call Stack(調用棧)以及Scope(做用域)能夠看到目前所處的做用域在Global全局做用域中.

Untitled.png

按F11或command + ; 執行下一步以下結果此時執行makePow函數,能夠看到調用棧Call Stack的棧頂爲makePow,而Scope做用域多了一個Local就是局部做用域裏面存儲着powerthis:Window 經過調試咱們能夠看到不少有用的信息,幫助咱們去理解程序.

Untitled 1.png

而後咱們讓程序執行到log的步驟執行的狀況,看下面的視圖,能夠看到Scope中有一個Script的做用域存儲着let變量的值,也就是let有一個單獨的做用域Script.

Untitled 2.png

後面的重點來了,而後咱們繼續往下執行一步,以下視圖能夠看到調用棧會執行power2()匿名函數,那麼這個匿名函數中power是從哪裏來的呢?看Scope部分多了一個Closure(makePow)它就是一個閉包,引用了makePowpower:2. 上述中講到的當閉包發生後外部函數會從調用棧移除掉,可是與閉包相關的變量會被緩存下來,這個例子緩存下來的就是power.

Untitled 3.png

在看一下執行power3的狀況,一樣緩存下來power:3 .這樣就是閉包的一個完整的過程.經過調試這樣就能夠很清晰的瞭解閉包的概念以及實現的過程比理解純理論上的東西要容易的多,因此所學習更多的是要掌握方法.

Untitled 4.png

純函數

純函數:相同的輸入永遠會獲得相同的輸出,並且沒有任何可觀察的反作用

  • 純函數就相似數學中的函數(用來描述輸入和輸出之間的關係),y=f(x);
  • lodash 是一個純函數的功能庫,提供了對數組、數字、對象、字符串、函數等操做的一些方法.
  • 數組的slice和splice分別是:純函數和不純的函數
    • slice 返回數組中的指定部分,不會改變原數組
    • splice 對數組進行操做返回該數組,會改變原數組
let array = [1,2,3,4,5];
console.log(array.slice(0,3));
console.log(array.slice(0,3));
console.log(array.slice(0,3));
//輸入相同 輸出也相同就是一個純函數
//[ 1, 2, 3 ]
// [ 1, 2, 3 ]
// [ 1, 2, 3 ]

//splice 就不是一個純函數 由於輸入相同可是每次的輸出結果不一樣
console.log(array.splice(0,3));
console.log(array.splice(0,3));
console.log(array.splice(0,3));
//splice 相同的輸入 每次輸出的結果不相同 那麼就是一個不純的函數
//[ 1, 2, 3 ]
//[ 4, 5 ]
//[]

//寫一個純函數
function getSum(n1,n2){
    return n1 + n2;
}
console.log(getSum(1,2));
console.log(getSum(1,2));
console.log(getSum(1,2));
// 3
// 3
// 3
複製代碼
  • 函數式編程不會保留計算中間的結果 因此變量是不可變的(無狀態的)
  • 咱們能夠把一個函數的執行結果交給另外一個函數去處理

Lodash 純函數的表明

lodash庫的使用,須要在nodejs的環境下引入lodash庫

//first last toUpper reverse each includes find findIndex
const _=require('lodash');
const array = ['jake','tom','lucy','kate'];

console.log(_.first(array));//jake 純函數
console.log(_.last(array));//kate 純函數
console.log(_.toUpper(_.first(array)));//JAKE 純函數

console.log(_.reverse(array));//[ 'kate', 'lucy', 'tom', 'jake' ] 注意:內部調用的是數組的reverse 而數組的reverse 會改變原有數組不是一個純函數的方法

const r = _.each(array,(item,index)=>{
    console.log(item,index);
});
console.log(r);

const l = _.find(array,(item)=>{
    return item === 'jake';
});
console.log(l,array);
複製代碼

純函數的好處

  • 可緩存:由於純函數對相同對輸入始終有相同的結果,因此能夠把純函數的結果緩存起來

lodash的memoize函數

const _ = require('lodash');

function getArea(r) {
    console.log(r);
    //計算圓的面積
    return Math.PI * r * r;
}
//lodash的memoize方法 接收一個純函數 對純函數的結果緩存 返回一個帶有記憶功能的函數
// let getAreaWithMemory = _.memoize(getArea);
// console.log(getAreaWithMemory(4));
// console.log(getAreaWithMemory(4));
// console.log(getAreaWithMemory(4));
/* 4 表示getArea這個函數只執行了一次 50.26548245743669 50.26548245743669 50.26548245743669 */
複製代碼

手動實現memoize函數

//模擬memoize方法的實現
function memoize(fn){
    let cache = {};
    return function(){
        //1 判斷cache是否有這個fn的結果
        let key = JSON.stringify(arguments);//將傳遞的參數做爲key
        cache[key] = cache[key] || fn.apply(fn,arguments);//若是沒有值調用fn() 結果做爲值
        return cache[key];
    }
}
let getAreaWithMemory = memoize(getArea);
console.log(getAreaWithMemory(4));
console.log(getAreaWithMemory(4));
console.log(getAreaWithMemory(4));
/* 結果以下: 4 50.26548245743669 50.26548245743669 50.26548245743669 */
複製代碼
  • 可測試 純函數讓測試更方便
  • 並行處理
    • 在多線程環境下並行操做共享的內存數據極可能會出現意外狀況
    • 純函數不須要訪問共享的內存數據,因此在並行環境下能夠任意運行純函數(Web Worker)

反作用

  • 純函數:對於相同的輸入永遠會獲得相同的輸出,並且沒有任何可觀察的反作用
//不純的函數 一旦mini的值發生了改變就會是函數變的不純 正是對外部的依賴致使的反作用
let mini = 18;
function checkAge(age){
    return age >= mini;
}

//純的 (硬編碼 後續會經過柯里化解決)
function makeCheckAge(age){
    let mini = 18;
    return age >= mini;
}
複製代碼

反作用讓一個函數變的不純,純函數的根據相同的輸入返回相同的輸出,若是函數依賴於外部的狀態就沒法保證輸出相同,就會帶來反作用.

反作用的來源

  • 配置文件
  • 數據庫
  • 獲取用戶的輸入
  • ...

全部的外部交互都有可能代來反作用,反作用也使得方法通用性降低不適合擴展和可重用性;同時反作用會給程序中帶來安全隱患給程序帶來不肯定性,可是反作用不可能徹底禁止,儘量控制它們在可控範圍內發生.

柯里化(Haskell Brooks Curry)

使用柯里化解決純函數的反作用.什麼是柯里化呢? 當函數有多個參數的時候,對函數進行改造調用一個函數只傳遞並返回一個新的函數(這部分參數之後永遠不會發生變化),這個新的函數去接收剩餘的參數,返回結果。

  • 使用柯里化解決上一個案例中硬編碼的問題
//硬編碼
function checkAge(age){
    let min = 18;
    return age >= min;
}

//解決硬編碼的問題 普通的純函數
function checkAge(min,age){
    return age >= min;
}

console.log(checkAge(18,20));//true

//解決基準值的問題 經過閉包的方式
function checkAge(min) {
    return function (age) {
        return age >= min;
    }
}
let checkAge = min => ((age) =>(age>=min));

let checkAge18 = checkAge(18);
let checkAge20 = checkAge(20);

console.log(checkAge18(20));
console.log(checkAge18(24));
console.log(checkAge20(20));
console.log(checkAge20(24));
複製代碼
  • lodash 中的柯里化的方法

lodash 通用的柯里化方法

curry(func) 建立一個函數而且該函數接收一個或多個func的參數,若是func所須要的參數,若是func所須要的參數都被提供則

則執行func並返回執行的結果,不然繼續返回該函數並等待接受剩餘的參數

參數:須要柯里化的函數

返回值:柯里化後的函數

const _ = require('lodash');
function getSum(a, b, c) {
    return a + b + c;
}
const curried = _.curry(getSum);

console.log(curried(1,2,3));
console.log(curried(1,2)(3));
console.log(curried(1)(2,3));
複製代碼
  • 柯里化的案例
//案例:提取字符串的空白字符
const match = curry(function (reg, str) {
    return str.match(reg);
});

const haveSpace = match(/\s+/g);
const haveNumber = match(/\d+/g);

const filter = curry(function(func,arry){
    return arry.filter(func);
});

console.log(haveSpace('hello world'));
console.log(haveNumber('123abc'));

console.log(filter(haveSpace,['jonm Connm','Jone_Done']));

const findSpace = filter(haveSpace);//新的函數 查找數組中具備空白數組的函數

console.log(findSpace(['jonm Connm','Jone_Done']));
複製代碼

閉包的本質就是內部函數能夠訪問外部函數的成員,而柯里化解決的是函數多個參數將函數進行分解的最小粒度的問題。要注意閉包和柯里化的區別兩個不是一個概念。

  • 柯里化的原理
//柯里化原理實現
        function curry(func) {
            return function curriedFn(...args) {
                //判斷匿名接受的參數個數以及func的形參個數
                if (args.length < func.length) {
                    //只傳遞部分的參數則返回一個新的函數
                    return function () {
                        //再次調用curriedFn 合併參數
                        return curriedFn(...args.concat(Array.from(arguments)));
                    }
                }
                //參數相同的狀況下直接調用func
                return func(...args);
            }
        }
        function getSum(a, b, c) {
            return a + b + c;
        }
        const curried = curry(getSum);

        console.log(curried(1, 2, 3));
        console.log(curried(1, 2)(3));
        console.log(curried(1)(2, 3));
複製代碼

這一塊是比較燒腦的,跟着調試工具來進行理解就很是容易理解了,以下圖所示:當執行到curried(1,2)(3)的時候,能夠看到在Closure的做用域中有兩個一個是傳入的func一個是分解的函數傳遞的值args[1,2]

Untitled 5.png

代碼繼續往下執行,會調用curriedFn()將上一次的參數和此次傳入的(3)進行合併,這時候arg.length==func.length,就會調用本來的函數func將全部的參數傳遞給它.

Untitled 6.png

  • 柯里化可讓咱們給一個函數傳遞較少的參數獲得一個已經記住了某些固定的新函數
  • 這是一種對函數參數的緩存
  • 讓函數變的更靈活,讓函數的粒度更小
  • 能夠把多元函數轉換成一元函數,能夠組合使用函數產生強大的功能。

函數組合

函數組合(compose):若是一個函數要通過多個函數處理才能獲得最終值,這個時候能夠把中間過程的函數合併成一個函數。函數就像是數據的管道,函數組合就是把這些管道鏈接起來,讓數據穿過多個管道造成最終結果。函數組合默認是從右到左執行.

  • 純函數和柯里化容易寫出洋蔥代碼 h(g(f(x)))
  • 函數組合能夠把細粒度的函數從新組合生成一個新的函數

以下例子,演示了函數組合

function compose(f, g) {
    return function (value) {
        return f(g(value));
    }
}

/* 演示函數組合的使用 */
function reverse(arr) {
    return arr.reverse();
}

function first(arr) {
    return arr[0];
}

const last = compose(first,reverse);
console.log(last([1,2,3,4,5]));
複製代碼

Lodash 中的組合函數,經過flowRight方法對函數進行組合,函數的執行順序從右到左

const _ = require('lodash');

const reverse = arr => arr.reverse();

const first = arr => arr[0];

const toUpper = s => s.toUpperCase();

const l = _.flowRight(toUpper, first, reverse);

console.log(l(['a', 'b', 'c', 'd', 'e']));
複製代碼

下面咱們來看看flowRight 的方法是如何實現的,這裏就要考到API掌握的程度了,數組的reducereverse 因爲數組的執行順序從左到右執行因此要講數組進行反轉調用reverse()方法,reduce方法是遍歷數組將上一個數組元素的值傳遞給下一個數組元素。這樣咱們就實現了組合函數,上一個函數的值傳遞給下一個函數。

//flowRight 的實現方法
function compose(...args) {
    console.log(args);
    return function (value) {
        return args.reverse().reduce(function (acc, fn) {
            return fn(acc);
        }, value);
    }
}
//獲取數組最後一個元素 轉換爲大寫 注意函數的運行順序從右到左
const l = compose(toUpper, first, reverse);
複製代碼

arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

第一個累計器累計回調的返回值; 它是上一次調用回調時返回的累積值

第二個參數數組中正在處理的元素。

將compose簡寫:經過ES6箭頭函數簡化代碼

const compose = (...args) => (value) => args.reverse().reduce((acc, fn) =>
    fn(acc), value);//reduce 第二個參數是一個初始的值 reduce是將全部數組進行遍歷好比累加第一個的結果會傳入到第二個中
複製代碼
  • 函數組合要知足結合律 既能夠把g和h組合,還可吧f和g組合,結果都是同樣的
let f = compose(f,g,h);
let a = compose(compose(f,g),h) == compose(f,compose(g,h))

//結合律
const f = _.flowRight(_.flowRight(_.toUpper,_.first),_.reverse);
===
const f = _.flowRight(_.toUpper,_.flowRight(_.first,_.reverse));

console.log(f(['a', 'b', 'c', 'd', 'e']));
複製代碼

組合函數如何調試

組合函數如何調試呢?好比我想打印某個方法執行的結果,其實處理很是簡單咱們只須要在想要打印某個方法的執行結果的方法後面添加一個方法tracetrace方法就是提供打印的方法,在該方法中能夠拿到上一個方法的返回值這樣就能夠打印上個一個方法的結果了,以下代碼所示:

/* 函數組合調試 */
//NEVER SAY DIE => never-say-die

const _ = require('lodash');
//_.split();

const split = _.curry((sep, str) => {
    return _.split(str, sep);
});

//toLower join
const join = _.curry((sep, arr) => {
    return _.join(arr, sep);
});

const trace = _.curry((tag,v)=>{
    console.log(tag,v);
    return v;
});

const map = _.curry((func,arr)=>{
    return _.map(arr,func);
})

const f = _.flowRight(join('-'),trace('map'), map(_.toLower),trace('split'),split(' '));

console.log(f('NEVER SAY DIE'));
複製代碼

lodash/fp 模塊

  • lodash 的fp模塊提供了實用的對函數式編程友好的方法。
  • 提供了不可變auto-curried iteratee-first data-last 的方法 函數優先數據滯後。

解決了上述中要使用curry進行柯里化的問題,有一些自帶的方法是先傳遞數據在傳遞迴調函數的,而fp模塊就是解決這種問題,將數據滯後。(PS:其實不一樣的語言和框架都是爲了解決問題的,請不要忘記程序員的本質就是爲了解決問題)

以下代碼中,通常常見的方法好比map()第一個參數都須要傳遞數據才能夠執行,可是這樣就沒法作到柯里化的處理了,那就必須經過柯里化將該方法從新封裝一層以下代碼:這樣是很是很差的設計,那麼loadsh是否提供了這樣的解決方案呢?答案是確定的咱們來看fp模塊

const _ = require('lodash');

//_.split();

const split = _.curry((sep, str) => {
    return _.split(str, sep);
});

//toLower join

const join = _.curry((sep, arr) => {
    return _.join(arr, sep);
});

const log=function(v){
    console.log(v);
    return v;
}

const trace = _.curry((tag,v)=>{
    console.log(tag,v);
    return v;
});

const map = _.curry((func,arr)=>{
    return _.map(arr,func);
})

const f = _.flowRight(join('-'),trace('map'), map(_.toLower),trace('split'),split(' '));

console.log('??',f('NEVER SAY DIE'));
複製代碼

以下代碼,fp模塊對map、join、split對了處理,以函數優先數據滯後

const fp = require('lodash/fp');
const f = fp.flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' '));

console.log(f('NEVER SAY DIE'));//never_say_die
複製代碼

map方法的區別和fp模塊

以下代碼,在_.map中對某個數組執行將數組元素轉換爲Number類型,可是結果打印倒是:23 NaN 2 這是爲何呢?parseInt(s: string, radix?: number) radix 進制因此會存在問題致使2被轉換2進制了,而fp模塊的map只會向parseInt傳遞一個參數

console.log(_.map(['23','8','10'],parseInt));//23 NaN 2
//parseInt('23',0,array)
//parseInt('8',1,array)
//parseInt('10',2,array)

//fp 模塊就不會出現這種問題
//fp map 的函數的參數只有一個就是處理的參數
console.log(fp.map(parseInt,['23','8','10']));//23 8 10
複製代碼

PointFree

能夠把數據處理的過程定義成與數據無關的合成運算,不須要用到表明數據的那個參數,只要把簡單的運算步驟合成到一塊兒,在使用這種模式以前須要定義一些輔助的基本運算函數。

  • 不須要指明處理的數據
  • 只須要合成運算過程
  • 須要定義一些輔助的基本運算函數

PointFree 模式 不須要關心數據

const f = fp.flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' '));
複製代碼

案例演示,其實PointFree模式就是函數的組合,函數組合不須要處理數據的,返回的新函數來處理數據

//Hello world => hello_world

const fp = require('lodash/fp');

const f = fp.flowRight(fp.replace(/\s+/g,'_'),fp.toLower);//函數組合不須要處理數據
//返回新的函數來處理數據
console.log(f('Hello world'));
複製代碼

下面咱們在寫一個案例來更深刻的理解PointFree模式

//world wild web => W,W,W
//先切割字符串變成數組,map將數組的每個元素轉換爲大寫,map將數組獲取數組的元素的首字母
const firstLetterToUpper = fp.flowRight(fp.join(', '),
fp.map(fp.flowRight(fp.first,fp.toUpper)),fp.split(' '));

console.log(firstLetterToUpper('world wild web'));
複製代碼

Functor(函子)

函數式編程中如何控制反作用控制在可控的範圍內、異常處理、異步操做等。這些問題引入了函子的概念

Fuctor函子

  • 容器:包含值和值的變形關係(這個變形關係就是函數)
  • 函子:是一個特殊的容器,經過一個普通的對象來實現,該對象具備map方法,map方法能夠運行一個函數對值進行處理(變形關係)

函子裏面內部維護一個值,這個值永遠不對外暴露,經過map方法來對值進行處理,經過一個鏈式的調用方式。

class Container {
    static of(value) {
        return new Container(value);
    }
    constructor(value) {
        this._value = value;
    }

    map(fn) {
        return Container.of(fn(this._value));
    }
}

let r = Container.of(5)
    .map(x => x + 1)
    .map(x => x * x);
console.log(r);//Container { _value: 36 }
複製代碼

總結:

  • 函數式編程的運算不直接操做值,而是由函子完成
  • 函子就是一個實現了map的契約對象
  • 能夠把函子想象成一個盒子,這個盒子裏面封裝了一個值
  • 想要處理盒子中的值,須要盒子的map方法傳遞一個處理值的函數(純函數),由這個函數來對值進行處理
  • 最終map方法返回一個包含新值的盒子(函子)

存在的問題,在輸入null的時候存在異常,沒法處理異常狀況,那麼如何解決這種的反作用呢?繼續看下面

//演示null undefined的問題
Container.of(null).map(x=>x.toUpperCase());//TypeError: Cannot read property 'toUpperCase' of null
複製代碼

MayBe 函子

MayBe函子的做用就是能夠對外部的控制狀況作處理

class MayBe {
    static of(value) {
        return new MayBe(value);
    }

    constructor(value) {
        this._value = value;
    }

    map(fn) {
        return this.isNoting() ? MayBe.of(null) : MayBe.of(fn(this._value));
    }

    isNoting() {
        return this._value === null || this._value === undefined;
    }
}

// let r = MayBe.of('hello world').map(x => x.toUpperCase());
// let r = MayBe.of(null).map(x => x.toUpperCase());//MayBe { _value: null }
let r = MayBe.of('hello world')
    .map(x => x.toUpperCase())
    .map(x => null)
    .map(x => x.split(' '));//MayBe { _value: null } 可是那個地方出現了問題呢? 是沒法知道的

//maybe 函子的問題

console.log(r);
複製代碼

MayBe 函子其實就是在容器的內部判斷值是否爲空,若是爲空就返回一個值爲空的函子。可是MayBe函子沒法知道哪一個地方出現了問題,如法處理異常問題,這就繼續引出了下一個概念。

Either 函子

Either 二者中的任何一個,相似if...else...的處理。異常會讓函數變的不純,Either函子能夠用來作異常處理,這種函子在經常使用的業務開發中會常常用到務必掌握。

以下代碼,定義兩個函子,一個處理正確的結果,一個處理異常的結果,異常的處理直接返回this

class Left {
    constructor(value) {
        this._value = value;
    }

    static of(value) {
        return new Left(value);
    }

    map(fn) {
        return this;
    }
}

class Right {
    constructor(value) {
        this._value = value;
    }

    static of(value) {
        return new Right(value);
    }

    map(fn) {
        return Right.of(fn(this._value));
    }
}
複製代碼

注意相同的輸入在兩個函子中是不一樣的輸出

let r1 = Right.of(12)
    .map(x => x + 2);

let l1 = Left.of(12).map(x => x + 2);

console.log(r1,l1);//Right { _value: 14 } Left { _value: 12 }
複製代碼

下面來演示,異常的處理狀況,以下代碼在catch中調用Left函子返回錯誤的結果

function parseJson(str){
    try {
        return Right.of(JSON.parse(str))
    } catch (e) {
        //出現錯誤的時候 使用Left 由於相同的輸入 獲得相同的輸出
        return Left.of({error:e.message});
    }
}

//異常狀況的處理
let r = parseJson('{ "name":"zs" }');

console.log(r);//Left { _value: { error: 'Unexpected token n in JSON at position 1' } }
複製代碼

正常的結果處理狀況,經過.map對下一步的業務邏輯進一步處理

//正確狀況下的處理
let r = parseJson('{ "name":"zs" }').map(x=>x.name.toUpperCase());//處理json將name屬性轉換爲大寫
console.log(r);//Right { _value: { name: 'ZS' } }
複製代碼

IO函子

IO 函子中的_value是一個函數,這裏把函數做爲值來處理;IO函子能夠把不純的動做存儲到_value中,延遲執行這個不純的操做(惰性執行),包裝當前的操做把不純的操做交個調用者處理

//IO 函子
const fp = require('lodash/fp');

class IO {
    static of(value) {
        return new IO(function () {
            return value;
        });
    }
    constructor(fn) {
        this._value = fn;
    }
    map(fn){
        return new IO(fp.flowRight(fn,this._value));
    }
}

//調用
let io = IO.of(process).map(p=>p.execPath).map(p=>p.toUpperCase());
console.log(io);
//將組合的函數調用 先執行p.execPath 再執行:p=>p.toUpperCase() 注意map函數的執行順序
console.log(io._value());///Users/prim/.nvm/versions/node/v12.14.0/bin/node 執行方法
///USERS/PRIM/.NVM/VERSIONS/NODE/V12.14.0/BIN/NODE
複製代碼

Folktale

folktale 是一個標準的函數式編程庫,異步任務的實現過於複雜,使用folktale中的Task來演示.只提供了一些函數式處理的操做:compose、curry等一些函子Task、Either、Maybe等

Task 函子處理異步任務

const { compose, curry } = require('folktale/core/lambda');
const { toUpper, first,split,find } = require('lodash/fp');
const { task } = require('folktale/concurrency/task');
const fs = require('fs');
let f = curry(2, (x, y) => {
    return x + y;
})

console.log(f(1, 2));//3
console.log(f(1)(2));//3

//compose 函數組合

let f1 = compose(toUpper, first);

console.log(f1(['one', 'two']));//ONE

function readFile(filename) {
    return task(resolver => {
        fs.readFile(filename, 'utf-8', (err, data) => {
            if (err) {
                resolver.reject(err);
            }
            resolver.resolve(data);
        })
    });
}

readFile('package.json')
    .map(split('\n'))
    .map(find(x=>x.includes('version')))
    .run()//?? run有什麼用?執行了什麼代碼呢? 是將上述的結果返回給listen嗎?
    .listen(
        {
            onRejected:err=>{
                console.log(err);
            },
            onResolved:data=>{
                console.log(data);
            }
        }
    );
複製代碼

Pointed函子

Pointed 函子是實現了of靜態方法的函子,of方法是爲了不使用new來建立對象,更深層的含義是of方法用來把值放到上下文Context(把值放到容器中,使用map來處理值)

其實上述將的函子都是Pointed函子。

Monad函子

IO函子的問題,在業務邏輯遇到函子嵌套的狀況IO(IO(x)); Monad就是解決函子嵌套問題的。

let readFile = function (filename) {
    return new IO(function () {
        return fs.readFileSync(filename, 'utf-8');
    });
}

let print = function (log) {
    return new IO(function(){
        console.log(log);
        return log;//log = IO(x)
    });
}

let cat = fp.flowRight(print,readFile);

let r = cat('package.json')._value()._value(); // IO(IO(x))
console.log(r);//IO { _value: [Function] }
複製代碼
  • Monad 函子是能夠變扁的Pointed函子
  • 一個函子若是具備join和of兩個方法並遵照一些定律就是一個Monad
const fp = require('lodash/fp');
const fs = require('fs');
class IO {
    static of(value) {
        return new IO(function () {
            return value;
        });
    }
    constructor(fn) {
        this._value = fn;
    }
    map(fn) {
        return new IO(fp.flowRight(fn, this._value));//合併函數返回一個新的函子
    }
    join(){
        //調用_value
        return this._value();
    }
    flatMap(fn){
        return this.map(fn).join();//把合併的函數 而後執行合併函數
    }
}
let readFile = function (filename) {
    return new IO(function () {
        return fs.readFileSync(filename, 'utf-8');
    });
}
let print = function (log) {
    return new IO(function(){
        console.log(log);
        return log;//log = IO(x)
    });
}
let r = readFile('package.json')//_value = fn1
    .map(x=>x.toUpperCase())//處理文件 _value=fn11
    .flatMap(print)//return IO(value) ==> _value = fp.flowRight(print,fn11,fn1); value = _value();
    .join(); // map(fn2) _value = fn2=new IO() ,fn1 join():_value: fp.flowRight(fn2, fn1) => new IO(fn3);---> join:fn3()
console.log(r);//IO { _value: [Function] }
複製代碼

總結

  • 函數式編程不能提升程序的性能,由於大量使用閉包在某種程度上會下降性能
  • 函數式編程中的函數不是程序中的函數和方法,而是數學中的函數
  • 函數式一等公民(MDN的解釋中只包含這三點)
    • 函數能夠存儲在變量中
    • 函數能夠做爲參數
    • 函數能夠做爲返回值
  • 反作用會讓一個函數變的不純,可是反作用是不可避免的,由於代碼不免會依賴外部文件、數據庫等,只能最大程度上控制反作用在可控的範圍內
  • 柯里化函數curry也是高階函數
  • 柯里化函數內部用到了閉包,對函數的參數作了緩存
  • 柯里化函數能夠把多個參數的函數轉換成只有一個參數的函數,經過組合產生功能更強大的函數
  • 柯里化讓函數變的更靈活,讓函數的粒度更小
  • 函數能夠看作一個處理數據的管道,管道中輸入參數 x,在管道中對數據處理後獲得結果 y
  • 經過函數組合能夠把多個一元函數組合成一個功能更強大的函數
  • 函數組合須要知足結合律,函數組合默認的執行順序是從右到左
  • 函子是一個特殊的容器(對象),這個容器內部封裝一個值,經過 map 傳遞一個函數對值進行處理
  • MayBe 函子的做用是處理外部的空值狀況,防止空值的異常
  • IO 函子內部封裝的值是一個函數,把不純的操做封裝到這個函數,不純的操做交給調用者處理
  • Monad 函子內部封裝的值是一個函數(這個函數返回函子),目的是經過 join 方法避免函子嵌套
相關文章
相關標籤/搜索