大前端進階-函數式編程

最近在學習大前端的進階,所以想把課程中學到的一些知識點結合這幾年的工做所感記錄下來。
漂亮的程序千千萬,有趣的思想各不一樣

何爲函數式編程

函數式編程是一種思想,能夠和麪向對象編程和麪向過程編程並列。html

  1. 面向過程,以過程爲中心的編程思想,分析出解決問題所須要的步驟,而後用函數把這些步驟一步一步實現,使用的時候一個一個依次調用。
  2. 面向對象,以對象爲中心的編程思想,通俗的講,就是把現實世界中的某個或者一組事物抽象成爲一個對象,經過屬性和方法描述其應該具備能力,利用繼承,多態等方法描述其變化。(我的以爲,面向過程編程的關鍵點是如何找到事物的相同點,並按照必定的規則將其設計爲對象。)
  3. 函數式編程,顧名思義,以函數爲中心的編程思想,可是須要注意的是,此函數非咱們常規意義上寫代碼時寫的函數,更趨向數學上的函數,即x => y的推導過程f(x),當f(x)固定後,必定有一個可推導且固定的y值與x值相對應。

函數式編程好處

函數式編程包含如下好處:前端

  1. 超級方便的代碼複用(我的感受如今公司中的部分前端開發將複製粘貼也看成了代碼複用的一種,當和他們提出既然多個頁面都用到,爲何不把這個處理邏輯提出來爲一個公用的方法呢,獲得的回答是粘貼一下就行了,爲何要提出來?額,其實邏輯的使用者不須要關心你內部邏輯是怎麼實現的,只須要能保證我輸入一組變量,獲得我想要的結果就好了)。
  2. 無this(vue2.0對ts支持不是很友好就倒在了這個this上,vue3.0就提出了Composition API解決代碼複用和ts支持)。
  3. 方便treeshaking(指的是代碼打包過程當中,經過分析,能夠將無用代碼剔除,只保留有用代碼,從而減小打包體積,優化加載性能)。
  4. 方便測試(我的感受在編寫前端單元測試用例的時候,若是某段邏輯對外的依賴越強,那麼測試用例越很差寫,所以在開發的時候經過合理的拆分邏輯可以方便編寫測試用例。那麼,測試用例是否方便編寫是否是衡量邏輯單元是否合理的標誌呢?)。

函數式編程特性

函數是一等公民

所謂一等公民指的是函數與其餘數據類型同樣,處於平等地位,能夠賦值給其餘變量,也能夠做爲參數,傳入另外一個函數,或者做爲別的函數的返回值。(一等公民 = 啥均可以 ?)vue

# 變量值
let handler = function () {}
# 參數
let forEach = function (array, handleFn) {}
# 返回值
let handler = function () {
    return function () {

    }
}
# 實例化
let handler = new Function()

高階函數

高階函數指的是能夠傳遞一個函數做爲參數的函數,通俗的講,高階函數也是一個函數,只不過它的參數中有一個是函數。node

高階函數的終極好處是:屏蔽實現細節,關注具體目標。

上文中的forEach就是一個高階函數,屏蔽實現細節指的是使用者不用關心函數內部是如何對數組進行遍歷,如何獲取數組中的每一個元素。關注具體目標指的是,使用者只關係在獲取到數組中的每一個元素後須要作什麼操做。編程

閉包

函數和對其周圍狀態( lexical environment,詞法環境)的引用捆綁在一塊兒構成 閉包closure)。也就是說,閉包可讓你從內部函數訪問外部函數做用域。在 JavaScript 中,每當函數被建立,就會在函數生成時生成閉包。
  1. 閉包是伴隨js函數產生的。
  2. 閉包是在函數做用域內引入非其做用域內的外部狀態。

以once函數展現基本的閉包數組

function once(fn) {
    let done = false
    return function() {
        // 在函數內部做用域內引入外部做用域的done狀態,使得done不會隨着once的執行完畢被銷燬,延長其做用時間
        if(!done) {
            done = true
            fn.apply(fn, arguments)
        }
    }
}

閉包的本質:函數執行完畢以後會被執行棧釋放,可是因爲其做用域中的狀態被外部引用,因此引用的狀態不能被釋放,還能夠被使用。緩存

純函數

前提: 函數必須有輸入輸出。
要求: 相同的輸入永遠會獲得相同的輸出(輸入輸出的數據流是顯式的),沒有可觀察的反作用。多線程

反作用是指當調用時,除了返回值以外,還對主調用產生附加的影響。反作用的不只僅只是返回了一個值,並且還作了其餘的事情。通俗的講就是函數依賴了外部狀態或者修改了外部狀態。
函數式編程要求函數無反作用,可是反作用是沒法徹底消除,只能將其控制在可控的範圍內。

爲何會有純函數(純函數有哪些好處)?閉包

  • 因爲輸入輸出能夠相互對應,所以能夠針對純函數的計算結果作緩存。
function momerize(fn) {
    let cache = {}
    return function(...args) {
        let key = JSON.stringify(args)
        cache[key] = cache[key] || fn.apply(fn, args)
        return cache[key]
    }
}
  • 因爲純函數沒有反作用,因此方便測試。
  • 因爲純函數沒有反作用,因此能夠在多線程中調用,能夠並行處理。
    js雖然是單線程的,可是最新的標準中添加了WebWork API,支持異步操做。
# 建立者
let worker = new Worker('test.js')
// 向執行者傳遞數據
worker.postMessage(1)
worker.onmessage = function(evt) {
    // 執行者返回的數據
    console.log(evt.data)
}
# 執行者 test.js
onmessage = function(evt) {
    postMessage(evt.data + 1)
}

## 感受和electron中主窗口和其餘窗口之間通訊很類似

柯里化

柯里化(Currying)是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數且返回結果的新函數的技術。

柯里化是對純函數的封裝,將一個多元(包含多個參數)純函數變爲可連續調用的多元或一元函數。也能夠理解爲,經過柯里化,能夠將函數細粒化,達到最大限度的代碼重用。app

// 簡單的柯里化函數
function curry(fn) {
    return function curriedFn(...args) {
        // 若是傳入的實參個數和fn的形參個數不同,那麼返回一個函數
        // 調用fn.length能夠獲取形參個數
        if (args.length < fn.length) {
            return function () {
                return curriedFn(...args.concat(Array.from(arguments)))
            }
        }
        // 若是相同,則調用fn返回結果
        return fn.apply(fn, args)
    }
}

// 多元函數
function sum(a, b, c) {
    return a + b + c
}
// 正常調用
console.log(sum(1, 2, 3))
let curried = curry(sum)
// 柯里化調用
console.log(curried(1)(2)(3))

函數組合

現實編程的過程當中,會出現這樣的狀況,一個數據須要通過多個函數的處理以後才能成爲你想要的數據,那麼調用時可能會出現相似y = n(g(f(x)))這樣的「洋蔥「式代碼,這種代碼既不利於理解,也不利於調試。想要避開這種寫法,就能夠利用函數組合。函數組合就是將多個細粒化的純函數組裝成一個函數,數據能夠在這個組裝後的函數中按照必定的順序執行。

// 簡單的組合函數
function compose(...args) {
    return function () {
        // reverse 反轉是爲了實現從右到左一次執行
        return args.reverse().reduce(function (result, fn) {
            return fn(...result)
        }, arguments)
    }
}
// 下面三個純函數是爲了實現獲取數組的最後一項並大寫
function reverse(array) {
    return array.reverse()
}

function first(array) {
    return array[0]
}

function toUpper(str) {
    return str.toUpperCase()
}

const arr = ['a', 'b', 'c']

// 原始調用
console.log(toUpper(first(reverse(arr))))

const composed = compose(toUpper, first, reverse)
// 組合後調用
console.log(composed(arr))

從上例中能夠看出,若是想要函數組合,那麼有個必要前提:被組合的函數必須是有輸入輸出的純函數。

函子

函子是函數式編程中最重要的數據類型,也是基本的運算單位和功能單位。

函子是兩個範疇之間的一種映射(關係)

什麼是範疇?
範疇是一個數學概念,通俗的講,當某組事物之間存在某種關係,經過這種關係能夠將事物組中的某個事物轉變爲另外一個事物,那麼這組事物和他們之間的關係就能夠構成一個範疇。兩個範疇之間能夠相互轉換,函子就是描述範疇之間如何轉換(經過函子能夠將一個範疇轉換爲另外一個範疇)。

函數式編程中最基本的一個函子以下:函子能夠看做是一個盒子,盒子中保存一個數據,調用者可經過map方法操做盒子中的數據。

class Functor {
    // 經過提供靜態的of方法,可使調用者避開new(new更趨向面向對象)
    static of(value) {
        return new Functor(value)
    }
    // 存儲外部傳遞的數據,將數據封閉,不對外開放
    constructor(value) {
        this._value = value
    }

    // 外部經過map方法傳遞如何處理存儲的數據,並將結果變爲一個新的函子(能夠實現鏈式操做)
    map(fn) {
        return Functor.of(fn(this._value))
    }
}

Maybe函子

函子能夠接受任意函數,用於處理內部的數據,可是當函子內部數據爲null的時候,map處理時會抱錯。
Functor.of(null).map(x => x.toUpperCase())
Maybe函子就是爲了解決這種問題,在其map處理數據的時候會判斷數據是否爲空。

class Maybe extends Functor {

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

    map(fn) {
        return this.isEmpty() ? Maybe.of(null) : Maybe.of(fn(this._value))
    }
}

Either函子

Either函子用來描述if...else...,所以它內部須要兩個值right和left,右值是正常狀況下使用的值,左值是右值不存在時使用的默認值。

Either常見的使用場景有兩個:

  1. 添加默認值
  2. 替代try...catch
class Either {
    static of(left, right) {
        return new Either(left, right)
    }
    constructor(left, right) {
        this._left = left
        this._right = right
    }
    isEmpty() {
        return this._right === null || this._right === undefined
    }

    map(fn) {
        return this.isEmpty() ? Either.of(fn(this._left), this._right) : Either.of(this._left, fn(this._right))
    }
}
const user = {}
// 提供默認值
Either.of({ name: 'zs' }, user.name).map(
    u => {
        console.log(u.name)
        return u.name
    }
)
// 替代try...catch
function toUpper(str) {
    try {
        Either.of(null, str.toUpperCase())
    } catch (e) {
        Either.of(e, null)
    }
}

IO函子

在純函數一節中提到過函數的反作用,咱們應該將反作用控制在必定範圍以內,IO函子就是爲了解決這一問題,經過將有反作用的數據包裝起來,讓調用方決定如何使用這部分數據。

class IO {
    // value 是指有反作用的數據
    static of(value) {
        return new IO(function () {
            return value
        })
    }
    constructor(fn) {
        // fn方法用於包裝有反作用的數據,在調用者真正使用數據的時候返回數據
        this._value = fn
    }

    map(fn) {
        // 用到了組合函數compose,將fn和value組成一個新的函數
        return new IO(compose(fn, this._value))
    }
}

// 調用: 獲取node的執行路徑
const io = IO.of(process).map(x => x.execPath)
console.log(io._value())

Monad函子

函子是一個盒子,內部包含了一個數據,函子也能夠看做一個數據,那麼就會出現函子內部的數據也是一個函子,即出現了函子的層層嵌套。Monad函子就是爲了解決此問題,經過join和flatMap方法解嵌套。

上文中的IO函子上面加上join,flatMap方法,其也能夠稱爲Monad函子。

class IO {
    // value 是指有反作用的數據
    static of(value) {
        return new IO(function () {
            return value
        })
    }
    constructor(fn) {
        // fn方法用於包裝有反作用的數據,在調用者真正使用數據的時候返回數據
        this._value = fn
    }

    map(fn) {
        // 用到了組合函數compose,將fn和value組成一個新的函數
        return new IO(compose(fn, this._value))
    }

    join() {
        return this._value()
    }

    flatMap(fn) {
        return this.map(fn).join()
    }
}

const fs = require('fs')
let readFile = function (filename) {
    return new IO(function () {
        return fs.readFileSync(filename, 'utf-8')
    })
}
let print = function (x) {
    return new IO(function () {
        console.log(x)
        return x
    })
}

let result = readFile('test.html')
    // 將讀取到的內容轉爲大寫
    .map(x => x.toUpperCase())
    // 因爲print函數返回一個函子,那麼flatMap能夠揭開第一層嵌套,返回print返回的函子
    .flatMap(print)
    // 獲取函子的結果
    .join()
console.log(result)
相關文章
相關標籤/搜索