原文地址:medium.com/javascript-…javascript
函數式編程已然變成了一個javascript語言中一個很是熱門的話題。僅在幾年之前,僅有少數的js程序員知道函數式編程是什麼。可是在過去三年中,我所見過的每一個大型應用代碼庫裏都使用了函數式編程概念。前端
函數式編程(常常縮寫爲FP)是經過組合純函數,避免共享狀態、可變數據、和反作用來構建軟件的過程。函數式編程是聲明性的而不是命令式的,應用狀態流經純函數中。相比於面向對象編程,其中的應用狀態常常是共享的,而且和方法一塊兒定義在一些對象中。java
函數式編程是一種編程模式。意味着它是一種基於一些基本原理和定義原則(如上所列)來思考軟件構造的方式。其它的編程模式還包括面向對象編程和過程式編程。node
相比於命令式的和麪向對象式的代碼,函數式的代碼趨向於更簡潔、更加可預言的、更容易測試。但若是你還不熟悉函數式編程以及它相關聯的一些基本模式,函數式的代碼看起來會更加緊湊,與之相關的文獻對於初學者來講也會比較費解。git
若是你開始谷歌搜索函數式編程時,你將很快會遇到大量的很是專業的學術性術語,這對初學者來講是很是嚇人的。說它有學習曲線就過輕描淡寫了。但若是你已經寫過js代碼經驗,頗有可能你已經在真實的軟件中使用了大量的函數式編程概念和工具。程序員
最難的部分是理解全部不熟悉的詞彙。上面那些看似可有可無的定義包含許多概念,這些概念須要在你掌握函數式編程的含義以前理解:github
換句話說,若是你想知道函數式編程在實踐中表明着什麼含義,那麼你不得不從理解這些核心概念開始。編程
純函數:redux
函數組合: 函數組合是將兩個或更多的函數組合成一個新函數或者執行一些計算的過程。例如,在javascript中,組成 f . g (.點表明組成)等價於f(g(x))。在理解軟件是如何使用函數式編程構建時,理解函數組合是很是重要的一步。 可閱讀什麼是函數組合瞭解更多。數組
共享狀態: 共享狀態是任意變量、對象或者是內存空間其存在於共享的做用域中,或者是做爲一個對象的屬性在各個做用域中傳遞。共享做用域包含全局做用域或者是閉包。常常,在面向對象編程中,在做用域中共享對象是經過將其添加爲其餘對象的屬性。
例如,一個計算機遊戲可能有一個主要的遊戲對象,該對象包含一些任務角色和遊戲項目做爲它擁有的屬性。函數式編程避免共享的狀態—相反它依賴不可變的數據結構和純計算從已有的數據中獲取新數據。 更多關於函數式的軟件是如何處理應用狀態的,可參考10個關於得到更好的redux 架構的技巧
共享狀態的問題在於,爲了理解一個函數的效果,你必須知道每一個共享變量在函數中怎麼使用和產生影響的整個歷史。
想象一下你有一個用戶對象須要保存。你的saveUser()函數發送一個API請求到服務端。在這個請求發送過程當中,用戶更改用戶頭像:updateAvatar()並觸發了另外一個saveUser()請求。在保存時,服務器發送回一個權威的用戶對象用於替換在內存中的數據以同步發生在服務端的改變或者響應其它的API請求。
不幸的是,第二個響應結果比第一個響應結果在到達,因此當第一個(如今是過期的)響應到達時,新的用戶頭像將會在內存中被清除掉並用舊的頭像替代。這是一個競態條件的例子——是一個關於共享狀態存在的一個很是廣泛的缺陷。
另一個關於共享狀態存在的廣泛問題是改變函數的調用順序會引起一連串的失敗。由於做用在共享狀態的函數是具備時間依賴性的。
// With shared state, the order in which function calls are made
// changes the result of the function calls.
const x = {
val: 2
};
const x1 = () => x.val += 1;
const x2 = () => x.val *= 2;
x1();
x2();
console.log(x.val); // 6
// This example is exactly equivalent to the above, except...
const y = {
val: 2
};
const y1 = () => y.val += 1;
const y2 = () => y.val *= 2;
// ...the order of the function calls is reversed...
y2();
y1();
// ... which changes the resulting value:
console.log(y.val); // 5
複製代碼
當你避免共享狀態,時間和函數調用順序不會改變調用函數的結果。利用純函數,給定相同的輸入,你將始終獲得相同的輸出。這使得函數調用徹底獨立於其餘的函數調用,可完全簡化更改和重構。一個函數中的改變或者是函數調用的時間都不會影響和破壞程序的其它部分。
const x = {
val: 2
};
const x1 = x => Object.assign({}, x, { val: x.val + 1});
const x2 = x => Object.assign({}, x, { val: x.val * 2});
console.log(x1(x2(x)).val); // 5
const y = {
val: 2
};
// Since there are no dependencies on outside variables,
// we don't need different functions to operate on different // variables. // this space intentionally left blank // Because the functions don't mutate, you can call these
// functions as many times as you want, in any order,
// without changing the result of other function calls.
x2(y);
x1(y);
console.log(x1(x2(y)).val); // 5
複製代碼
在上面的例子中,咱們使用Object.assign()並傳遞一個空對象做爲第一個參數用來拷貝x的屬性而不是直接修改它。在這種狀況下,這就至關於不利用Object.assign()方法,從零開始簡單地建立一個新對象。但這在javascript中是一種很是常見的模式爲已存在的狀態建立拷貝副本而不是直接修改已有的狀態值,就如第一個例子所演示的同樣。
若是你仔細看一下這個例子中的console.log()語句,你應該會發現我前面提到過的一些概念:函數組合。回想一下前面的內容,函數組合應該是像這樣:f(g(x))。在這個例子中,咱們分別將f()和g()替換爲想x1()和x2()成爲組合x1 . x2。
固然,若是你改變組合的順序,輸出將會改變。運算順序是有影響的。f(g(x))不老是等於g(f(x)),可是在函數以外的變量發生了什麼變得再也不重要了,這纔是重要的事。若是使用非純函數,那麼久不可能徹底理解一個函數作了什麼,除非你瞭解函數使用和影響的每一個變量的整個歷史。
移除掉函數調用的時間依賴性,你會消除掉一整類的潛在的bug。
不變性: 不可變的對象是指一個對象一旦建立後不能對其修改。相反,可變的對象是指對象建立後可對其進行修改。
不可變性是函數式編程的核心概念。由於若是缺乏它,程序中的數據流將會有損耗。狀態歷史被遺棄的話,奇怪的bug將會蔓延到軟件中。關於更多不可變性的意義,可參考The Dao of Immutability。
在javascript中,不將const和不可變性混爲一談是很重要的。const是變量名綁定,變量建立後不能從新賦值。const不能建立不可變的對象。你不能夠改變對象的引用指向,可是你仍能夠改變對象上的屬性值。也就是說,用const建立的綁定是可變的而不是不可變的。
不可變的對象是徹底不能夠改變的。你能夠經過深度凍結對象作到一個值真正地不可變。JavaScript中有一個方法能夠凍結一個對象的一級深度。
const a = Object.freeze({
foo: 'Hello',
bar: 'world',
baz: '!'
});
a.foo = 'Goodbye';
// Error: Cannot assign to read only property 'foo' of object Object
複製代碼
可是,凍結對象只是表面上的不可變。例如,下面的對象是可變的:
const a = Object.freeze({
foo: { greeting: 'Hello' },
bar: 'world',
baz: '!'
});
a.foo.greeting = 'Goodbye';
console.log(`${ a.foo.greeting }, ${ a.bar }${a.baz}`);
複製代碼
正如你所看見的,一個凍結對象的頂層的簡單屬性是不能夠改變的,可是若是任意一個屬性是對象類型(包含數組等),那麼它仍然是能夠修改的。所以,即便是凍結的對象也不是不可變的,除非你遍歷整個對象樹並凍結每個對象類型屬性。
在許多函數式編程語言中,有一些特殊的不可變數據結構—trie data structures(讀做‘tree’)。它們是有效的深度凍結,意味着任何屬性都不能被更改,不管它位於對象的那一層級上。
針對對象的全部部分,Tries 使用共享結構來共享引用內存位置。在對象被一個操做拷貝以後,它們仍然是未被改變的。Tries使用了更少的內存,使得一些操做在性能上有很大提高。
例如,你能夠在對象樹上的根部使用身份對照用於對比。若是身份相同,那就無需遍歷整棵樹來檢查差別性。
在JavaScript中還有一些庫利用了tries的有點,包括immutable-js和mori
我已經嘗試過上面兩種,並趨向於在須要大量不可變狀態的大項目中使用Imuutable.js。更多相關內容請詳見10個關於得到更好的redux 架構的技巧。
反作用: 反作用是指任意的應用狀態變化在程序調用的外面都是可見的而不是做爲他的返回值。反作用包括:
更改任意的外部變量和對象屬性(如全局變量,或位於父函數做用域鏈中的變量)
輸出日誌到console
在屏幕上寫
寫文件
寫數據到網絡
觸發任意外部處理
調用任何包含反作用的其它函數
反作用在函數式編程中大多被避免可以使得程序的效果更容易被理解和測試。
Haskell 和其它函數式語言常用**monads**從純函數中隔離和封裝反作用。monads主題的內容足夠寫一本書,因此咱們將它放在後面。
你如今只須要知道的是反作用須要在你軟件剩下的部分中隔離出來。若是你保持反作用從剩下的程序邏輯中隔離出,那你的軟件將會變得更加容易擴展、重構、調試和維護。
這就是爲何大多數前端框架爲何鼓勵用戶分開管理狀態和組件渲染,弱耦合模塊。
利用高階函數達到可重用性 函數式編程趨向於重用一套通用的函數式的實用工具來處理數據。面向對象編程趨向於將方法和數據都放在對象中。這些同地協做的方法僅僅操做它們被設計好的指望操做的數據類型。並且常常是一些僅包含在特定對象實例中的數據。
在函數式編程中,任意數據類型都是場公平競爭的遊戲。相同的map工具可映射在對象、字符串、數字、或者任何其它類型數據上。由於它接受一個函數做爲參數並適當地處理給定的數據類型。FP使用高階函數實現了它的通用工具詭計。
JavaScript具備一級函數,這容許咱們將函數做爲數據賦值給變量,傳遞給其它函數,從函數中返回,等等。。。
高階函數是採用一個函數做爲參數,返回一個函數,或者二者兼具的一個函數。高階函數經常使用於:
前面咱們學習了相同的map工具可做用於各類類型的數據類型。它經過映射操做和一個functor API一塊兒工做完成目的。map()使用的重要流控制操做利用了接口的優勢。從Array.prototype.map()狀況來看,數組是container,可是其它數據結構也能夠是functors,只要它們提供映射API。
讓咱們看下Array.prototype.map()是怎麼容許你從映射工具中抽取數據類型使得map()能夠在任何數據類型下都是可用的。咱們將建立一個簡單的double()映射,它只是簡單的將傳進來值乘以2:
const double = n => n * 2;
const doubleMap = numbers => numbers.map(double);
console.log(doubleMap([2, 3, 4])); // [ 4, 6, 8 ]
複製代碼
若是咱們但願操做遊戲中的數據,將遊戲所得到的點數翻倍該怎麼辦呢?全部咱們須要作的是對傳遞給map()的double函數作一點微小的變更,而後全部的東西都會正常工做:
const double = n => n.points * 2;
const doubleMap = numbers => numbers.map(double);
console.log(doubleMap([
{ name: 'ball', points: 2 },
{ name: 'coin', points: 3 },
{ name: 'candy', points: 4}
])); // [ 4, 6, 8 ]
複製代碼
使用抽象(像functors和高階函數這樣爲了使用通用的實用工具函數來操做任意數量的不一樣數據類型)的概念對函數式編程是十分重要的,你將會看到一個相似的概念應用在各類不一樣途徑
*隨着時間表示的列表是流*
複製代碼
全部如今你須要理解就是數組和functors不是惟一的方式,讓容器這個概念和容器中的值來使用。好比,一個數組僅僅是一列東西。隨着時間表示的列表是流—因此你能夠應用相同類型的工具來處理到來的事件流—這是一些當你利用FP開始構建真實的軟件時常常看見的東西。
聲明式 VS 命令式 函數式編程是聲明式模式,意味着程序的邏輯的表達無需明確的流控制的描述。
命令式程序花費大量的代碼描述具體的步驟以獲取指望的結果—流控制:如何作。
聲明式程序抽象出流控制過程而不是花費大量的代碼描述數據流:作什麼?怎麼作(how)被抽象出來了。
舉個栗子,命令式的映射傳入一個數字數組並返回一個每一個數字都乘以2的新數組。
const doubleMap = numbers => {
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
return doubled;
};
console.log(doubleMap([2, 3, 4])); // [4, 6, 8]
複製代碼
聲明式的映射也是作一樣的事情,可是使用函數式的Array.prototype.map()工具將流控制抽象出來, 這就容許你更加清楚地表達數據流。
const doubleMap = numbers => numbers.map(n => n * 2);
console.log(doubleMap([2, 3, 4])); // [4, 6, 8]
複製代碼
命令式的代碼常常地使用陳述。陳述是一段用於執行一些動做的代碼。常用陳述的例子包含for, if, switch, throw等。
聲明式的代碼更對依賴於表達式。表達書是一段用來計算一些值得代碼片斷。表達式常常是結合一些函數調用、值和操做符求值產生結果值。
這些都是表達式的例子:
2 * 2
doubleMap([2, 3, 4])
Math.max(4, 3, 2)
複製代碼
在代碼中,你會看到一些表達式賦值給一些標識符,從函數中返回出來或者傳遞給函數。在賦值、返回或者傳遞以前,表達式先被求值,而後結果值被使用。
結論 函數式編程主張:
ps:歡迎指正翻譯不正之處。