最近在學習大前端的進階,所以想把課程中學到的一些知識點結合這幾年的工做所感記錄下來。
漂亮的程序千千萬,有趣的思想各不一樣
函數式編程是一種思想,能夠和麪向對象編程和麪向過程編程並列。html
函數式編程包含如下好處:前端
所謂一等公民指的是函數與其餘數據類型同樣,處於平等地位,能夠賦值給其餘變量,也能夠做爲參數,傳入另外一個函數,或者做爲別的函數的返回值。(一等公民 = 啥均可以 ?)vue
# 變量值 let handler = function () {} # 參數 let forEach = function (array, handleFn) {} # 返回值 let handler = function () { return function () { } } # 實例化 let handler = new Function()
高階函數指的是能夠傳遞一個函數做爲參數的函數,通俗的講,高階函數也是一個函數,只不過它的參數中有一個是函數。node
高階函數的終極好處是:屏蔽實現細節,關注具體目標。
上文中的forEach就是一個高階函數,屏蔽實現細節指的是使用者不用關心函數內部是如何對數組進行遍歷,如何獲取數組中的每一個元素。關注具體目標指的是,使用者只關係在獲取到數組中的每一個元素後須要作什麼操做。編程
函數和對其周圍狀態( lexical environment,詞法環境)的引用捆綁在一塊兒構成 閉包( closure)。也就是說,閉包可讓你從內部函數訪問外部函數做用域。在 JavaScript 中,每當函數被建立,就會在函數生成時生成閉包。
以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] } }
# 建立者 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)) } }
函子能夠接受任意函數,用於處理內部的數據,可是當函子內部數據爲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函子用來描述if...else...,所以它內部須要兩個值right和left,右值是正常狀況下使用的值,左值是右值不存在時使用的默認值。
Either常見的使用場景有兩個:
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函子就是爲了解決這一問題,經過將有反作用的數據包裝起來,讓調用方決定如何使用這部分數據。
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函子就是爲了解決此問題,經過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)