摘要:僅在幾年以前,僅有少數(shù)的程序員知道函數(shù)式編程是什么。函數(shù)式編程是聲明性的而不是命令式的應用狀態(tài)流經(jīng)純函數(shù)中。函數(shù)式編程是一種編程模式。在理解軟件是如何使用函數(shù)式編程構(gòu)建時,理解函數(shù)組合是非常重要的一步。不可變性是函數(shù)式編程的核心概念。
函數(shù)式編程已然變成了一個javascript語言中一個非常熱門的話題。僅在幾年以前,僅有少數(shù)的js程序員知道函數(shù)式編程是什么。但是在過去三年中,我所見過的每個大型應用代碼庫里都使用了函數(shù)式編程概念。
函數(shù)式編程(經(jīng)??s寫為FP)是通過組合純函數(shù),避免共享狀態(tài)、可變數(shù)據(jù)、和副作用來構(gòu)建軟件的過程。函數(shù)式編程是聲明性的而不是命令式的,應用狀態(tài)流經(jīng)純函數(shù)中。相比于面向?qū)ο缶幊?,其中的應用狀態(tài)經(jīng)常是共享的,并且和方法一起定義在一些對象中。
函數(shù)式編程是一種編程模式。意味著它是一種基于一些基本原理和定義原則(如上所列)來思考軟件構(gòu)造的方式。其它的編程模式還包括面向?qū)ο缶幊毯瓦^程式編程。
相比于命令式的和面向?qū)ο笫降拇a,函數(shù)式的代碼趨向于更簡潔、更加可預言的、更容易測試。但如果你還不熟悉函數(shù)式編程以及它相關聯(lián)的一些基本模式,函數(shù)式的代碼看起來會更加緊湊,與之相關的文獻對于初學者來說也會比較費解。
如果你開始谷歌搜索函數(shù)式編程時,你將很快會遇到大量的非常專業(yè)的學術性術語,這對初學者來說是非常嚇人的。說它有學習曲線就太輕描淡寫了。但如果你已經(jīng)寫過js代碼經(jīng)驗,很有可能你已經(jīng)在真實的軟件中使用了大量的函數(shù)式編程概念和工具。
不要讓所有的新詞匯嚇走你。它通常比聽起來更簡單。最難的部分是理解所有不熟悉的詞匯。上面那些看似無關緊要的定義包含許多概念,這些概念需要在你掌握函數(shù)式編程的含義之前理解:
純函數(shù)
函數(shù)組合
避免共享的狀態(tài)
避免改變狀態(tài)
避免副作用
換句話說,如果你想知道函數(shù)式編程在實踐中代表著什么含義,那么你不得不從理解這些核心概念開始。
純函數(shù):
給定相同的輸入,總是返回相同的輸出
沒有副作用
在函數(shù)式編程中,純函數(shù)有很多重要的特性。包括引用透明性(你可以將一個函數(shù)調(diào)用替換成它的結(jié)果值而不會改變程序的意義)。可閱讀什么是純函數(shù)了解更多。
函數(shù)組合:
函數(shù)組合是將兩個或更多的函數(shù)組合成一個新函數(shù)或者執(zhí)行一些計算的過程。例如,在javascript中,組成 f . g (.點代表組成)等價于f(g(x))。在理解軟件是如何使用函數(shù)式編程構(gòu)建時,理解函數(shù)組合是非常重要的一步。
可閱讀什么是函數(shù)組合了解更多。
共享狀態(tài):
共享狀態(tài)是任意變量、對象或者是內(nèi)存空間其存在于共享的作用域中,或者是作為一個對象的屬性在各個作用域中傳遞。共享作用域包含全局作用域或者是閉包。經(jīng)常,在面向?qū)ο缶幊讨校谧饔糜蛑泄蚕韺ο笫峭ㄟ^將其添加為其他對象的屬性。
例如,一個計算機游戲可能有一個主要的游戲?qū)ο?,該對象包含一些任務角色和游戲項目作為它擁有的屬性。函?shù)式編程避免共享的狀態(tài)—相反它依賴不可變的數(shù)據(jù)結(jié)構(gòu)和純計算從已有的數(shù)據(jù)中獲取新數(shù)據(jù)。
更多關于函數(shù)式的軟件是如何處理應用狀態(tài)的,可參考10個關于獲得更好的redux 架構(gòu)的技巧
共享狀態(tài)的問題在于,為了理解一個函數(shù)的效果,你必須知道每個共享變量在函數(shù)中怎么使用和產(chǎn)生影響的整個歷史。
想象一下你有一個用戶對象需要保存。你的saveUser()函數(shù)發(fā)送一個API請求到服務端。在這個請求發(fā)送過程中,用戶更改用戶頭像:updateAvatar()并觸發(fā)了另一個saveUser()請求。在保存時,服務器發(fā)送回一個權(quán)威的用戶對象用于替換在內(nèi)存中的數(shù)據(jù)以同步發(fā)生在服務端的改變或者響應其它的API請求。
不幸的是,第二個響應結(jié)果比第一個響應結(jié)果在到達,所以當?shù)谝粋€(現(xiàn)在是過時的)響應到達時,新的用戶頭像將會在內(nèi)存中被清除掉并用舊的頭像替代。這是一個競態(tài)條件的例子——是一個關于共享狀態(tài)存在的一個非常普遍的缺陷。
另外一個關于共享狀態(tài)存在的普遍問題是改變函數(shù)的調(diào)用順序會引發(fā)一連串的失敗。因為作用在共享狀態(tài)的函數(shù)是具有時間依賴性的。
// 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
當你避免共享狀態(tài),時間和函數(shù)調(diào)用順序不會改變調(diào)用函數(shù)的結(jié)果。利用純函數(shù),給定相同的輸入,你將始終得到相同的輸出。這使得函數(shù)調(diào)用完全獨立于其他的函數(shù)調(diào)用,可徹底簡化更改和重構(gòu)。一個函數(shù)中的改變或者是函數(shù)調(diào)用的時間都不會影響和破壞程序的其它部分。
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()并傳遞一個空對象作為第一個參數(shù)用來拷貝x的屬性而不是直接修改它。在這種情況下,這就相當于不利用Object.assign()方法,從零開始簡單地創(chuàng)建一個新對象。但這在javascript中是一種非常常見的模式為已存在的狀態(tài)創(chuàng)建拷貝副本而不是直接修改已有的狀態(tài)值,就如第一個例子所演示的一樣。
如果你仔細看一下這個例子中的console.log()語句,你應該會發(fā)現(xiàn)我前面提到過的一些概念:函數(shù)組合?;叵胍幌虑懊娴膬?nèi)容,函數(shù)組合應該是像這樣:f(g(x))。在這個例子中,我們分別將f()和g()替換為想x1()和x2()成為組合x1?. x2。
當然,如果你改變組合的順序,輸出將會改變。運算順序是有影響的。f(g(x))不總是等于g(f(x)),但是在函數(shù)之外的變量發(fā)生了什么變得不再重要了,這才是重要的事。如果使用非純函數(shù),那么久不可能完全理解一個函數(shù)做了什么,除非你了解函數(shù)使用和影響的每個變量的整個歷史。
移除掉函數(shù)調(diào)用的時間依賴性,你會消除掉一整類的潛在的bug。
不變性:
不可變的對象是指一個對象一旦創(chuàng)建后不能對其修改。相反,可變的對象是指對象創(chuàng)建后可對其進行修改。
不可變性是函數(shù)式編程的核心概念。因為如果缺少它,程序中的數(shù)據(jù)流將會有損耗。狀態(tài)歷史被遺棄的話,奇怪的bug將會蔓延到軟件中。關于更多不可變性的意義,可參考The Dao of Immutability。
在javascript中,不將const和不可變性混為一談是很重要的。const是變量名綁定,變量創(chuàng)建后不能重新賦值。const不能創(chuàng)建不可變的對象。你不可以改變對象的引用指向,但是你仍可以改變對象上的屬性值。也就是說,用const創(chuàng)建的綁定是可變的而不是不可變的。
不可變的對象是完全不可以改變的。你可以通過深度凍結(jié)對象做到一個值真正地不可變。JavaScript中有一個方法可以凍結(jié)一個對象的一級深度。
const a = Object.freeze({ foo: "Hello", bar: "world", baz: "!" }); a.foo = "Goodbye"; // Error: Cannot assign to read only property "foo" of object Object
但是,凍結(jié)對象只是表面上的不可變。例如,下面的對象是可變的:
const a = Object.freeze({ foo: { greeting: "Hello" }, bar: "world", baz: "!" }); a.foo.greeting = "Goodbye"; console.log(`${ a.foo.greeting }, ${ a.bar }${a.baz}`);
正如你所看見的,一個凍結(jié)對象的頂層的簡單屬性是不可以改變的,但是如果任意一個屬性是對象類型(包含數(shù)組等),那么它仍然是可以修改的。因此,即使是凍結(jié)的對象也不是不可變的,除非你遍歷整個對象樹并凍結(jié)每一個對象類型屬性。
在許多函數(shù)式編程語言中,有一些特殊的不可變數(shù)據(jù)結(jié)構(gòu)—trie data structures(讀作‘tree’)。它們是有效的深度凍結(jié),意味著任何屬性都不能被更改,無論它位于對象的那一層級上。
針對對象的所有部分,Tries 使用共享結(jié)構(gòu)來共享引用內(nèi)存位置。在對象被一個操作拷貝之后,它們?nèi)匀皇俏幢桓淖兊摹ries使用了更少的內(nèi)存,使得一些操作在性能上有很大提升。
例如,你可以在對象樹上的根部使用身份對照用于對比。如果身份相同,那就無需遍歷整棵樹來檢查差異性。
在JavaScript中還有一些庫利用了tries的有點,包括immutable-js和mori
我已經(jīng)嘗試過上面兩種,并趨向于在需要大量不可變狀態(tài)的大項目中使用Imuutable.js。更多相關內(nèi)容請詳見10個關于獲得更好的redux 架構(gòu)的技巧。
副作用:
副作用是指任意的應用狀態(tài)變化在程序調(diào)用的外面都是可見的而不是作為他的返回值。副作用包括:
更改任意的外部變量和對象屬性(如全局變量,或位于父函數(shù)作用域鏈中的變量)
輸出日志到console
在屏幕上寫
寫文件
寫數(shù)據(jù)到網(wǎng)絡
觸發(fā)任意外部處理
調(diào)用任何包含副作用的其它函數(shù)
副作用在函數(shù)式編程中大多被避免可使得程序的效果更容易被理解和測試。
Haskell 和其它函數(shù)式語言經(jīng)常使用monads從純函數(shù)中隔離和封裝副作用。monads主題的內(nèi)容足夠?qū)懸槐緯?,所以我們將它放在后面?/p>
你現(xiàn)在只需要知道的是副作用需要在你軟件剩下的部分中隔離出來。如果你保持副作用從剩下的程序邏輯中隔離出,那你的軟件將會變得更加容易擴展、重構(gòu)、調(diào)試和維護。
這就是為什么大多數(shù)前端框架為什么鼓勵用戶分開管理狀態(tài)和組件渲染,弱耦合模塊。
利用高階函數(shù)達到可重用性
函數(shù)式編程趨向于重用一套通用的函數(shù)式的實用工具來處理數(shù)據(jù)。面向?qū)ο缶幊腾呄蛴趯⒎椒ê蛿?shù)據(jù)都放在對象中。這些同地協(xié)作的方法僅僅操作它們被設計好的期望操作的數(shù)據(jù)類型。而且經(jīng)常是一些僅包含在特定對象實例中的數(shù)據(jù)。
在函數(shù)式編程中,任意數(shù)據(jù)類型都是場公平競爭的游戲。相同的map工具可映射在對象、字符串、數(shù)字、或者任何其它類型數(shù)據(jù)上。因為它接受一個函數(shù)作為參數(shù)并適當?shù)靥幚斫o定的數(shù)據(jù)類型。FP使用高階函數(shù)實現(xiàn)了它的通用工具詭計。
JavaScript具有一級函數(shù),這允許我們將函數(shù)作為數(shù)據(jù)賦值給變量,傳遞給其它函數(shù),從函數(shù)中返回,等等。。。
高階函數(shù)是采用一個函數(shù)作為參數(shù),返回一個函數(shù),或者兩者兼具的一個函數(shù)。高階函數(shù)常用于:
抽取或者隔離動作,影響或者使用回調(diào)函數(shù),promise, monads等的異步流控制
創(chuàng)建可作用于各種各樣數(shù)據(jù)類型的實用工具
部分應用一個函數(shù)到它的參數(shù)或者創(chuàng)建一個柯里化函數(shù)達到重用或者函數(shù)組合的目的。
接受一系列函數(shù)并返回這些輸入函數(shù)的一些組合
Containers, Functors, Lists, and Streams
functor是指可用于映射的東西。換句話說,它是一個容器,包含一個可應用一個函數(shù)到它內(nèi)部數(shù)據(jù)的接口。當你看見functor這個詞時,你應該想到可映射的(mappable)。
前面我們學習了相同的map工具可作用于各種類型的數(shù)據(jù)類型。它通過映射操作和一個functor API一起工作完成目的。map()使用的重要流控制操作利用了接口的優(yōu)點。從Array.prototype.map()情況來看,數(shù)組是container,但是其它數(shù)據(jù)結(jié)構(gòu)也可以是functors,只要它們提供映射API。
讓我們看下Array.prototype.map()是怎么允許你從映射工具中抽取數(shù)據(jù)類型使得map()可以在任何數(shù)據(jù)類型下都是可用的。我們將創(chuàng)建一個簡單的double()映射,它只是簡單的將傳進來值乘以2:
const double = n => n * 2; const doubleMap = numbers => numbers.map(double); console.log(doubleMap([2, 3, 4])); // [ 4, 6, 8 ]
如果我們希望操作游戲中的數(shù)據(jù),將游戲所獲得的點數(shù)翻倍該怎么辦呢?所有我們需要做的是對傳遞給map()的double函數(shù)做一點微小的變動,然后所有的東西都會正常工作:
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和高階函數(shù)這樣為了使用通用的實用工具函數(shù)來操作任意數(shù)量的不同數(shù)據(jù)類型)的概念對函數(shù)式編程是十分重要的,你將會看到一個類似的概念應用在各種不同途徑
*隨著時間表示的列表是流*
所有現(xiàn)在你需要理解就是數(shù)組和functors不是唯一的方式,讓容器這個概念和容器中的值來使用。比如,一個數(shù)組僅僅是一列東西。隨著時間表示的列表是流—所以你可以應用相同類型的工具來處理到來的事件流—這是一些當你利用FP開始構(gòu)建真實的軟件時經(jīng)??匆姷臇|西。
聲明式 VS 命令式
函數(shù)式編程是聲明式模式,意味著程序的邏輯的表達無需明確的流控制的描述。
命令式程序花費大量的代碼描述具體的步驟以獲取期望的結(jié)果—流控制:如何做。
聲明式程序抽象出流控制過程而不是花費大量的代碼描述數(shù)據(jù)流:做什么?怎么做(how)被抽象出來了。
舉個栗子,命令式的映射傳入一個數(shù)字數(shù)組并返回一個每個數(shù)字都乘以2的新數(shù)組。
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]
聲明式的映射也是做同樣的事情,但是使用函數(shù)式的Array.prototype.map()工具將流控制抽象出來,
這就允許你更加清楚地表達數(shù)據(jù)流。
const doubleMap = numbers => numbers.map(n => n * 2); console.log(doubleMap([2, 3, 4])); // [4, 6, 8]
命令式的代碼經(jīng)常地使用陳述。陳述是一段用于執(zhí)行一些動作的代碼。經(jīng)常使用陳述的例子包含for, if, switch, throw等。
聲明式的代碼更對依賴于表達式。表達書是一段用來計算一些值得代碼片段。表達式經(jīng)常是結(jié)合一些函數(shù)調(diào)用、值和操作符求值產(chǎn)生結(jié)果值。
這些都是表達式的例子:
2 * 2 doubleMap([2, 3, 4]) Math.max(4, 3, 2)
在代碼中,你會看到一些表達式賦值給一些標識符,從函數(shù)中返回出來或者傳遞給函數(shù)。在賦值、返回或者傳遞之前,表達式先被求值,然后結(jié)果值被使用。
結(jié)論
函數(shù)式編程主張:
純函數(shù)而不是共享狀態(tài)和副作用
基于可變數(shù)據(jù)的不可變性
基于命令式流控制的函數(shù)組合
大量的通用的,可重用的工具使用高階函數(shù)作用于多種數(shù)據(jù)類型而不是只能在它們共同協(xié)作的數(shù)據(jù)上操作。
聲明式的代碼而不是命令式的代碼(做什么而非怎么做)
表達式而不是陳述
基于即時多態(tài)的容器和高階函數(shù)
ps:歡迎指正翻譯不正之處。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/96583.html
摘要:插件開發(fā)前端掘金作者原文地址譯者插件是為應用添加全局功能的一種強大而且簡單的方式。提供了與使用掌控異步前端掘金教你使用在行代碼內(nèi)優(yōu)雅的實現(xiàn)文件分片斷點續(xù)傳。 Vue.js 插件開發(fā) - 前端 - 掘金作者:Joshua Bemenderfer原文地址: creating-custom-plugins譯者:jeneser Vue.js插件是為應用添加全局功能的一種強大而且簡單的方式。插....
摘要:首次運行代碼時,會創(chuàng)建一個全局執(zhí)行上下文并到當前的執(zhí)行棧中。執(zhí)行上下文的創(chuàng)建執(zhí)行上下文分兩個階段創(chuàng)建創(chuàng)建階段執(zhí)行階段創(chuàng)建階段確定的值,也被稱為。 (關注福利,關注本公眾號回復[資料]領取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實戰(zhàn)、面試指導) 本周正式開始前端進階的第一期,本周的主題是調(diào)用堆棧,,今天是第一天 本計劃一共28期,每期重點攻克一個面試重難點,如果你還不了解本進...
摘要:在嚴格模式下調(diào)用函數(shù)則不影響默認綁定?;卣{(diào)函數(shù)丟失綁定是非常常見的。因為直接指定的綁定對象,稱之為顯示綁定。調(diào)用時強制把的綁定到上顯示綁定無法解決丟失綁定問題。 (關注福利,關注本公眾號回復[資料]領取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實戰(zhàn)、面試指導) 本周正式開始前端進階的第三期,本周的主題是this全面解析,今天是第9天。 本計劃一共28期,每期重點攻克一個面試重...
摘要:然而學習布局,你只要學習幾個手機端頁面自適應解決方案布局進階版附源碼示例前端掘金一年前筆者寫了一篇手機端頁面自適應解決方案布局,意外受到很多朋友的關注和喜歡。 十分鐘學會 Fiddler - 后端 - 掘金一.Fiddler介紹 Fiddler是一個http抓包改包工具,fiddle英文中有欺騙、偽造之意,與wireshark相比它更輕量級,上手簡單,因為只能抓http和https數(shù)據(jù)...
摘要:使用異步編程,有一個事件循環(huán)。它作為面向?qū)ο缶幊痰奶娲桨?,其中應用狀態(tài)通常與對象中的方法搭配并共享。在用面向?qū)ο缶幊虝r遇到不同的組件競爭相同的資源的時候,更是如此。 翻譯:瘋狂的技術宅原文:https://www.indeed.com/hire/i... 本文首發(fā)微信公眾號:jingchengyideng歡迎關注,每天都給你推送新鮮的前端技術文章 不管你是面試官還是求職者,里面...
閱讀 1226·2021-11-15 18:00
閱讀 1813·2021-10-08 10:15
閱讀 804·2021-09-04 16:48
閱讀 2417·2021-09-04 16:48
閱讀 1342·2019-08-29 18:40
閱讀 991·2019-08-29 13:08
閱讀 3016·2019-08-26 14:06
閱讀 1135·2019-08-26 13:35