摘要:僅在幾年以前,僅有少數的程序員知道函數式編程是什么。函數式編程是聲明性的而不是命令式的應用狀態流經純函數中。函數式編程是一種編程模式。在理解軟件是如何使用函數式編程構建時,理解函數組合是非常重要的一步。不可變性是函數式編程的核心概念。
函數式編程已然變成了一個javascript語言中一個非常熱門的話題。僅在幾年以前,僅有少數的js程序員知道函數式編程是什么。但是在過去三年中,我所見過的每個大型應用代碼庫里都使用了函數式編程概念。
函數式編程(經常縮寫為FP)是通過組合純函數,避免共享狀態、可變數據、和副作用來構建軟件的過程。函數式編程是聲明性的而不是命令式的,應用狀態流經純函數中。相比于面向對象編程,其中的應用狀態經常是共享的,并且和方法一起定義在一些對象中。
函數式編程是一種編程模式。意味著它是一種基于一些基本原理和定義原則(如上所列)來思考軟件構造的方式。其它的編程模式還包括面向對象編程和過程式編程。
相比于命令式的和面向對象式的代碼,函數式的代碼趨向于更簡潔、更加可預言的、更容易測試。但如果你還不熟悉函數式編程以及它相關聯的一些基本模式,函數式的代碼看起來會更加緊湊,與之相關的文獻對于初學者來說也會比較費解。
如果你開始谷歌搜索函數式編程時,你將很快會遇到大量的非常專業的學術性術語,這對初學者來說是非常嚇人的。說它有學習曲線就太輕描淡寫了。但如果你已經寫過js代碼經驗,很有可能你已經在真實的軟件中使用了大量的函數式編程概念和工具。
不要讓所有的新詞匯嚇走你。它通常比聽起來更簡單。最難的部分是理解所有不熟悉的詞匯。上面那些看似無關緊要的定義包含許多概念,這些概念需要在你掌握函數式編程的含義之前理解:
純函數
函數組合
避免共享的狀態
避免改變狀態
避免副作用
換句話說,如果你想知道函數式編程在實踐中代表著什么含義,那么你不得不從理解這些核心概念開始。
純函數:
給定相同的輸入,總是返回相同的輸出
沒有副作用
在函數式編程中,純函數有很多重要的特性。包括引用透明性(你可以將一個函數調用替換成它的結果值而不會改變程序的意義)。可閱讀什么是純函數了解更多。
函數組合:
函數組合是將兩個或更多的函數組合成一個新函數或者執行一些計算的過程。例如,在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具有一級函數,這允許我們將函數作為數據賦值給變量,傳遞給其它函數,從函數中返回,等等。。。
高階函數是采用一個函數作為參數,返回一個函數,或者兩者兼具的一個函數。高階函數常用于:
抽取或者隔離動作,影響或者使用回調函數,promise, monads等的異步流控制
創建可作用于各種各樣數據類型的實用工具
部分應用一個函數到它的參數或者創建一個柯里化函數達到重用或者函數組合的目的。
接受一系列函數并返回這些輸入函數的一些組合
Containers, Functors, Lists, and Streams
functor是指可用于映射的東西。換句話說,它是一個容器,包含一個可應用一個函數到它內部數據的接口。當你看見functor這個詞時,你應該想到可映射的(mappable)。
前面我們學習了相同的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:歡迎指正翻譯不正之處。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/96583.html
摘要:插件開發前端掘金作者原文地址譯者插件是為應用添加全局功能的一種強大而且簡單的方式。提供了與使用掌控異步前端掘金教你使用在行代碼內優雅的實現文件分片斷點續傳。 Vue.js 插件開發 - 前端 - 掘金作者:Joshua Bemenderfer原文地址: creating-custom-plugins譯者:jeneser Vue.js插件是為應用添加全局功能的一種強大而且簡單的方式。插....
摘要:首次運行代碼時,會創建一個全局執行上下文并到當前的執行棧中。執行上下文的創建執行上下文分兩個階段創建創建階段執行階段創建階段確定的值,也被稱為。 (關注福利,關注本公眾號回復[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導) 本周正式開始前端進階的第一期,本周的主題是調用堆棧,,今天是第一天 本計劃一共28期,每期重點攻克一個面試重難點,如果你還不了解本進...
摘要:在嚴格模式下調用函數則不影響默認綁定。回調函數丟失綁定是非常常見的。因為直接指定的綁定對象,稱之為顯示綁定。調用時強制把的綁定到上顯示綁定無法解決丟失綁定問題。 (關注福利,關注本公眾號回復[資料]領取優質前端視頻,包括Vue、React、Node源碼和實戰、面試指導) 本周正式開始前端進階的第三期,本周的主題是this全面解析,今天是第9天。 本計劃一共28期,每期重點攻克一個面試重...
摘要:然而學習布局,你只要學習幾個手機端頁面自適應解決方案布局進階版附源碼示例前端掘金一年前筆者寫了一篇手機端頁面自適應解決方案布局,意外受到很多朋友的關注和喜歡。 十分鐘學會 Fiddler - 后端 - 掘金一.Fiddler介紹 Fiddler是一個http抓包改包工具,fiddle英文中有欺騙、偽造之意,與wireshark相比它更輕量級,上手簡單,因為只能抓http和https數據...
摘要:使用異步編程,有一個事件循環。它作為面向對象編程的替代方案,其中應用狀態通常與對象中的方法搭配并共享。在用面向對象編程時遇到不同的組件競爭相同的資源的時候,更是如此。 翻譯:瘋狂的技術宅原文:https://www.indeed.com/hire/i... 本文首發微信公眾號:jingchengyideng歡迎關注,每天都給你推送新鮮的前端技術文章 不管你是面試官還是求職者,里面...
閱讀 1193·2021-11-15 18:00
閱讀 1789·2021-10-08 10:15
閱讀 752·2021-09-04 16:48
閱讀 2373·2021-09-04 16:48
閱讀 1313·2019-08-29 18:40
閱讀 965·2019-08-29 13:08
閱讀 2987·2019-08-26 14:06
閱讀 1111·2019-08-26 13:35