摘要:函數(shù)式編程導(dǎo)論從屬于筆者的前端入門與工程實踐。函數(shù)式編程即是在軟件開發(fā)的工程中避免使用共享狀態(tài)可變狀態(tài)以及副作用。
JavaScript 函數(shù)式編程JavaScript 函數(shù)式編程導(dǎo)論從屬于筆者的Web 前端入門與工程實踐。本文很多地方是講解函數(shù)式編程的優(yōu)勢,就筆者個人而言是認可函數(shù)式編程具有一定的好處,但是不推崇徹底的函數(shù)式編程化,特別是對于復(fù)雜應(yīng)用邏輯的開發(fā)。筆者在應(yīng)用的狀態(tài)管理工具中就更傾向于使用MobX而不是Redux,詳見2016-我的前端之路:工具化與工程化。
近年來,函數(shù)式編程(Functional Programming)已經(jīng)成為了JavaScript社區(qū)中炙手可熱的主題之一,無論你是否欣賞這種編程理念,相信你都已經(jīng)對它有所了解。即使是前幾年函數(shù)式編程尚未流行的時候,我已經(jīng)在很多的大型應(yīng)用代碼庫中發(fā)現(xiàn)了不少對于函數(shù)式編程理念的深度實踐。函數(shù)式編程即是在軟件開發(fā)的工程中避免使用共享狀態(tài)(Shared State)、可變狀態(tài)(Mutable Data)以及副作用(Side Effects)。函數(shù)式編程中整個應(yīng)用由數(shù)據(jù)驅(qū)動,應(yīng)用的狀態(tài)在不同純函數(shù)之間流動。與偏向命令式編程的面向?qū)ο缶幊潭裕瘮?shù)式編程其更偏向于聲明式編程,代碼更加簡潔明了、更可預(yù)測,并且可測試性也更好。。函數(shù)式編程本質(zhì)上也是一種編程范式(Programming Paradigm),其代表了一系列用于構(gòu)建軟件系統(tǒng)的基本定義準則。其他編程范式還包括面向?qū)ο缶幊蹋∣bject Oriented Programming)與過程程序設(shè)計(Procedural Programming)。
純函數(shù)顧名思義,純函數(shù)往往指那些僅根據(jù)輸入?yún)?shù)決定輸出并且不會產(chǎn)生任何副作用的函數(shù)。純函數(shù)最優(yōu)秀的特性之一在于其結(jié)果的可預(yù)測性:
var z = 10; function add(x, y) { return x + y; } console.log(add(1, 2)); // prints 3 console.log(add(1, 2)); // still prints 3 console.log(add(1, 2)); // WILL ALWAYS print 3
在add函數(shù)中并沒有操作z變量,即沒有讀取z的數(shù)值也沒有修改z的值。它僅僅根據(jù)參數(shù)輸入的x與y變量然后返回二者相加的和。這個add函數(shù)就是典型的純函數(shù),而如果在add函數(shù)中涉及到了讀取或者修改z變量,那么它就失去了純潔性。我們再來看另一個函數(shù):
function justTen() { return 10; }
對于這樣并沒有任何輸入?yún)?shù)的函數(shù),如果它要保持為純函數(shù),那么該函數(shù)的返回值就必須為常量。不過像這種固定返回為常量的函數(shù)還不如定義為某個常量呢,就沒必要大材小用用函數(shù)了,因此我們可以認為絕大部分的有用的純函數(shù)至少允許一個輸入?yún)?shù)。再看看下面這個函數(shù):
function addNoReturn(x, y) { var z = x + y }
注意這個函數(shù)并沒有返回任何值,它確實擁有兩個輸入?yún)?shù)x與y,然后將這兩個變量相加賦值給z,因此這樣的函數(shù)也可以認為是無意義的。這里我們可以說,絕大部分有用的純函數(shù)必須要有返回值。總結(jié)而言,純函數(shù)應(yīng)該具有以下幾個特效:
絕大部分純函數(shù)應(yīng)該擁有一或多個參數(shù)值。
純函數(shù)必須要有返回值。
相同輸入的純函數(shù)的返回值必須一致。
純函數(shù)不能夠產(chǎn)生任何的副作用。
共享狀態(tài)與副作用共享狀態(tài)(Shared State)可以是存在于共享作用域(全局作用域與閉包作用域)或者作為傳遞到不同作用域的對象屬性的任何變量、對象或者內(nèi)存空間。在面向?qū)ο缶幊讨校覀兂3J峭ㄟ^添加屬性到其他對象的方式共享某個對象。共享狀態(tài)問題在于,如果開發(fā)者想要理解某個函數(shù)的作用,必須去詳細了解該函數(shù)可能對于每個共享變量造成的影響。譬如我們現(xiàn)在需要將客戶端生成的用戶對象保存到服務(wù)端,可以利用saveUser()函數(shù)向服務(wù)端發(fā)起請求,將用戶信息編碼傳遞過去并且等待服務(wù)端響應(yīng)。而就在你發(fā)起請求的同時,用戶修改了個人頭像,觸發(fā)了另一個函數(shù)updateAvatar()以及另一次saveUser()請求。正常來說,服務(wù)端會先響應(yīng)第一個請求,并且根據(jù)第二個請求中用戶參數(shù)的變更對于存儲在內(nèi)存或者數(shù)據(jù)庫中的用戶信息作相應(yīng)的修改。不過某些意外情況下,可能第二個請求會比第一個請求先到達服務(wù)端,這樣用戶選定的新的頭像反而會被第一個請求中的舊頭像覆寫。這里存放在服務(wù)端的用戶信息就是所謂的共享狀態(tài),而因為多個并發(fā)請求導(dǎo)致的數(shù)據(jù)一致性錯亂也就是所謂的競態(tài)條件(Race Condition),也是共享狀態(tài)導(dǎo)致的典型問題之一。另一個共享狀態(tài)的常見問題在于不同的調(diào)用順序可能會觸發(fā)未知的錯誤,這是因為對于共享狀態(tài)的操作往往是時序依賴的。
const x = { val: 2 }; const x1 = () => x.val += 1; const x2 = () => x.val *= 2; x1(); x2(); console.log(x.val); // 6 const y = { val: 2 }; const y1 = () => y.val += 1; const y2 = () => y.val *= 2; // 交換了函數(shù)調(diào)用順序 y2(); y1(); // 最后的結(jié)果也受到了影響 console.log(y.val); // 5
副作用指那些在函數(shù)調(diào)用過程中沒有通過返回值表現(xiàn)的任何可觀測的應(yīng)用狀態(tài)變化,常見的副作用包括但不限于:
修改任何外部變量或者外部對象屬性
在控制臺中輸出日志
寫入文件
發(fā)起網(wǎng)絡(luò)通信
觸發(fā)任何外部進程事件
調(diào)用任何其他具有副作用的函數(shù)
在函數(shù)式編程中我們會盡可能地規(guī)避副作用,保證程序更易于理解與測試。Haskell或者其他函數(shù)式編程語言通常會使用Monads來隔離與封裝副作用。在絕大部分真實的應(yīng)用場景進行編程開始時,我們不可能保證系統(tǒng)中的全部函數(shù)都是純函數(shù),但是我們應(yīng)該盡可能地增加純函數(shù)的數(shù)目并且將有副作用的部分與純函數(shù)剝離開來,特別是將業(yè)務(wù)邏輯抽象為純函數(shù),來保證軟件更易于擴展、重構(gòu)、調(diào)試、測試與維護。這也是很多前端框架鼓勵開發(fā)者將用戶的狀態(tài)管理與組件渲染相隔離,構(gòu)建松耦合模塊的原因。
不變性不可變對象(Immutable Object)指那些創(chuàng)建之后無法再被修改的對象,與之相對的可變對象(Mutable Object)指那些創(chuàng)建之后仍然可以被修改的對象。不可變性(Immutability)是函數(shù)式編程的核心思想之一,保證了程序運行中數(shù)據(jù)流的無損性。如果我們忽略或者拋棄了狀態(tài)變化的歷史,那么我們很難去捕獲或者復(fù)現(xiàn)一些奇怪的小概率問題。使用不可變對象的優(yōu)勢在于你在程序的任何地方訪問任何的變量,你都只有只讀權(quán)限,也就意味著我們不用再擔心意外的非法修改的情況。另一方面,特別是在多線程編程中,每個線程訪問的變量都是常量,因此能從根本上保證線程的安全性。總結(jié)而言,不可變對象能夠幫助我們構(gòu)建簡單而更加安全的代碼。
在JavaScript中,我們需要搞清楚const與不可變性之間的區(qū)別。const聲明的變量名會綁定到某個內(nèi)存空間而不可以被二次分配,其并沒有創(chuàng)建真正的不可變對象。你可以不修改變量的指向,但是可以修改該對象的某個屬性值,因此const創(chuàng)建的還是可變對象。JavaScript中最方便的創(chuàng)建不可變對象的方法就是調(diào)用Object.freeze()函數(shù),其可以創(chuàng)建一層不可變對象:
const a = Object.freeze({ foo: "Hello", bar: "world", baz: "!" }); a.foo = "Goodbye"; // Error: Cannot assign to read only property "foo" of object Object
不過這種對象并不是徹底的不可變數(shù)據(jù),譬如如下的對象就是可變的:
const a = Object.freeze({ foo: { greeting: "Hello" }, bar: "world", baz: "!" }); a.foo.greeting = "Goodbye"; console.log(`${ a.foo.greeting }, ${ a.bar }${a.baz}`);
如上所見,頂層的基礎(chǔ)類型屬性是不可以改變的,不過如果對象類型的屬性,譬如數(shù)組等,仍然是可以變化的。在很多函數(shù)式編程語言中,會提供特殊的不可變數(shù)據(jù)結(jié)構(gòu)Trie Data Structures來實現(xiàn)真正的不可變數(shù)據(jù)結(jié)構(gòu),任何層次的屬性都不可以被改變。Tries還可以利用結(jié)構(gòu)共享(Structural Sharing)的方式來在新舊對象之間共享未改變的對象屬性值,從而減少內(nèi)存占用并且顯著提升某些操作的性能。JavaScript中雖然語言本身并沒有提供給我們這個特性,但是可以通過Immutable.js與Mori這些輔助庫來利用Tries的特性。我個人兩個庫都使用過,不過在大型項目中會更傾向于使用Immutable.js。估計到這邊,很多習(xí)慣了命令式編程的同學(xué)都會大吼一句:在沒有變量的世界里我又該如何編程呢?不要擔心,現(xiàn)在我們考慮下我們何時需要去修改變量值:譬如修改某個對象的屬性值,或者在循環(huán)中修改某個循環(huán)計數(shù)器的值。而函數(shù)式編程中與直接修改原變量值相對應(yīng)的就是創(chuàng)建原值的一個副本并且將其修改之后賦予給變量。而對于另一個常見的循環(huán)場景,譬如我們所熟知的for,while,do,repeat這些關(guān)鍵字,我們在函數(shù)式編程中可以使用遞歸來實現(xiàn)原本的循環(huán)需求:
// 簡單的循環(huán)構(gòu)造 var acc = 0; for (var i = 1; i <= 10; ++i) acc += i; console.log(acc); // prints 55 // 遞歸方式實現(xiàn) function sumRange(start, end, acc) { if (start > end) return acc; return sumRange(start + 1, end, acc + start) } console.log(sumRange(1, 10, 0)); // prints 55
注意在遞歸中,與變量i相對應(yīng)的即是start變量,每次將該值加1,并且將acc+start作為當前和值傳遞給下一輪遞歸操作。在遞歸中,并沒有修改任何的舊的變量值,而是根據(jù)舊值計算出新值并且進行返回。不過如果真的讓你把所有的迭代全部轉(zhuǎn)變成遞歸寫法,估計得瘋掉,這個不可避免地會受到JavaScript語言本身的混亂性所影響,并且迭代式的思維也不是那么容易理解的。而在Elm這種專門面向函數(shù)式編程的語言中,語法會簡化很多:
sumRange start end acc = if start > end then acc else sumRange (start + 1) end (acc + start)
其每一次的迭代記錄如下:
sumRange 1 10 0 = -- sumRange (1 + 1) 10 (0 + 1) sumRange 2 10 1 = -- sumRange (2 + 1) 10 (1 + 2) sumRange 3 10 3 = -- sumRange (3 + 1) 10 (3 + 3) sumRange 4 10 6 = -- sumRange (4 + 1) 10 (6 + 4) sumRange 5 10 10 = -- sumRange (5 + 1) 10 (10 + 5) sumRange 6 10 15 = -- sumRange (6 + 1) 10 (15 + 6) sumRange 7 10 21 = -- sumRange (7 + 1) 10 (21 + 7) sumRange 8 10 28 = -- sumRange (8 + 1) 10 (28 + 8) sumRange 9 10 36 = -- sumRange (9 + 1) 10 (36 + 9) sumRange 10 10 45 = -- sumRange (10 + 1) 10 (45 + 10) sumRange 11 10 55 = -- 11 > 10 => 55 55高階函數(shù)
函數(shù)式編程傾向于重用一系列公共的純函數(shù)來處理數(shù)據(jù),而面向?qū)ο缶幊虅t是將方法與數(shù)據(jù)封裝到對象內(nèi)。這些被封裝起來的方法復(fù)用性不強,只能作用于某些類型的數(shù)據(jù),往往只能處理所屬對象的實例這種數(shù)據(jù)類型。而函數(shù)式編程中,任何類型的數(shù)據(jù)則是被一視同仁,譬如map()函數(shù)允許開發(fā)者傳入函數(shù)參數(shù),保證其能夠作用于對象、字符串、數(shù)字,以及任何其他類型。JavaScript中函數(shù)同樣是一等公民,即我們可以像其他類型一樣處理函數(shù),將其賦予變量、傳遞給其他函數(shù)或者作為函數(shù)返回值。而高階函數(shù)(Higher Order Function)則是能夠接受函數(shù)作為參數(shù),能夠返回某個函數(shù)作為返回值的函數(shù)。高階函數(shù)經(jīng)常用在如下場景:
利用回調(diào)函數(shù)、Promise或者Monad來抽象或者隔離動作、作用以及任何的異步控制流
構(gòu)建能夠作用于泛數(shù)據(jù)類型的工具函數(shù)
函數(shù)重用或者創(chuàng)建柯里函數(shù)
將輸入的多個函數(shù)并且返回這些函數(shù)復(fù)合而來的復(fù)合函數(shù)
典型的高階函數(shù)的應(yīng)用就是復(fù)合函數(shù),作為開發(fā)者,我們天性不希望一遍一遍地重復(fù)構(gòu)建、測試與部分相同的代碼,我們一直在尋找合適的只需要寫一遍代碼的方法以及如何將其重用于其他模塊。代碼重用聽上去非常誘人,不過其在很多情況下是難以實現(xiàn)的。如果你編寫過于偏向具體業(yè)務(wù)的代碼,那么就會難以重用。而如果你把每一段代碼都編寫的過于泛化,那么你就很難將這些代碼應(yīng)用于具體的有業(yè)務(wù)場景,而需要編寫額外的連接代碼。而我們真正追尋的就是在具體與泛化之間尋求一個平衡點,能夠方便地編寫短小精悍而可復(fù)用的代碼片,并且能夠?qū)⑦@些小的代碼片快速組合而解決復(fù)雜的功能需求。
在函數(shù)式編程中,函數(shù)就是我們能夠面向的最基礎(chǔ)代碼塊,而在函數(shù)式編程中,對于基礎(chǔ)塊的組合就是所謂的函數(shù)復(fù)合(Function Composition)。我們以如下兩個簡單的JavaScript函數(shù)為例:
var add10 = function(value) { return value + 10; }; var mult5 = function(value) { return value * 5; };
如果你習(xí)慣了使用ES6,那么可以用Arrow Function重構(gòu)上述代碼:
var add10 = value => value + 10; var mult5 = value => value * 5;
現(xiàn)在看上去清爽多了吧,下面我們考慮面對一個新的函數(shù)需求,我們需要構(gòu)建一個函數(shù),首先將輸入?yún)?shù)加10然后乘以5,我們可以創(chuàng)建一個新函數(shù)如下:
var mult5AfterAdd10 = value => 5 * (value + 10)
盡管上面這個函數(shù)也很簡單,我們還是要避免任何函數(shù)都從零開始寫,這樣也會讓我們做很多重復(fù)性的工作。我們可以基于上文的add10與mult5這兩個函數(shù)來構(gòu)建新的函數(shù):
var mult5AfterAdd10 = value => mult5(add10(value));
在mult5AfterAdd10函數(shù)中,我們已經(jīng)站在了add10與mult5這兩個函數(shù)的基礎(chǔ)上,不過我們可以用更優(yōu)雅的方式來實現(xiàn)這個需求。在數(shù)學(xué)中,我們認為f ° g是所謂的Function Composition,因此`f ° g可以認為等價于f(g(x)),我們同樣可以基于這種思想重構(gòu)上面的mult5AfterAdd10。不過JavaScript中并沒有原生的Function Composition支持,在Elm中我們可以用如下寫法:
add10 value = value + 10 mult5 value = value * 5 mult5AfterAdd10 value = (mult5 << add10) value
這里的<<操作符也就指明了在Elm中是如何組合函數(shù)的,同時也較為直觀的展示出了數(shù)據(jù)的流向。首先value會被賦予給add10,然后add10的結(jié)果會流向mult5。另一個需要注意的是,(mult5 << add10)中的中括號是為了保證函數(shù)組合會在函數(shù)調(diào)用之前。你也可以組合更多的函數(shù):
f x = (g << h << s << r << t) x
如果在JavaScript中,你可能需要以如下的遞歸調(diào)用來實現(xiàn)該功能:
g(h(s(r(t(x)))))
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/81159.html
摘要:循環(huán)的函數(shù)式改造翻譯自。循環(huán)的設(shè)計思想深受可變狀態(tài)與副作用的影響,不過函數(shù)式編程中認為可變狀態(tài)與副作用是導(dǎo)致潛在錯誤與不可預(yù)測性的罪魁禍首,是應(yīng)該盡力避免的模式。 JavaScript For 循環(huán)的函數(shù)式改造翻譯自Rethinking JavaScript: Death of the For Loop。前兩天筆者整理了一篇JavaScript 函數(shù)式編程導(dǎo)論,筆者個人不是很喜歡徹底的...
摘要:前端進階進階構(gòu)建項目一配置最佳實踐狀態(tài)管理之痛點分析與改良開發(fā)中所謂狀態(tài)淺析從時間旅行的烏托邦,看狀態(tài)管理的設(shè)計誤區(qū)使用更好地處理數(shù)據(jù)愛彼迎房源詳情頁中的性能優(yōu)化從零開始,在中構(gòu)建時間旅行式調(diào)試用輕松管理復(fù)雜狀態(tài)如何把業(yè)務(wù)邏輯這個故事講好和 前端進階 webpack webpack進階構(gòu)建項目(一) Webpack 4 配置最佳實踐 react Redux狀態(tài)管理之痛點、分析與...
摘要:本文最早為雙十一而作,原標題雙大前端工程師讀書清單,以付費的形式發(fā)布在上。發(fā)布完本次預(yù)告后,捕捉到了一個友善的吐槽讀書清單也要收費。這本書便從的異步編程講起,幫助我們設(shè)計快速響應(yīng)的網(wǎng)絡(luò)應(yīng)用,而非簡單的頁面。 本文最早為雙十一而作,原標題雙 11 大前端工程師讀書清單,以付費的形式發(fā)布在 GitChat 上。發(fā)布之后在讀者圈群聊中和讀者進行了深入的交流,現(xiàn)免費分享到這里,不足之處歡迎指教...
摘要:本文最早為雙十一而作,原標題雙大前端工程師讀書清單,以付費的形式發(fā)布在上。發(fā)布完本次預(yù)告后,捕捉到了一個友善的吐槽讀書清單也要收費。這本書便從的異步編程講起,幫助我們設(shè)計快速響應(yīng)的網(wǎng)絡(luò)應(yīng)用,而非簡單的頁面。 本文最早為雙十一而作,原標題雙 11 大前端工程師讀書清單,以付費的形式發(fā)布在 GitChat 上。發(fā)布之后在讀者圈群聊中和讀者進行了深入的交流,現(xiàn)免費分享到這里,不足之處歡迎指教...
閱讀 2070·2021-11-23 09:51
閱讀 3364·2021-09-28 09:36
閱讀 1138·2021-09-08 09:35
閱讀 1783·2021-07-23 10:23
閱讀 3279·2019-08-30 15:54
閱讀 3014·2019-08-29 17:05
閱讀 451·2019-08-29 13:23
閱讀 1307·2019-08-28 17:51