javascript
学会JavaScript函数式编程(第1部分)
2019獨角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
摘要: JS函數(shù)式編程入門。
- 原文:學(xué)會使用函數(shù)式編程的程序員(第1部分)
- 作者:前端小智
Fundebug經(jīng)授權(quán)轉(zhuǎn)載,版權(quán)歸原作者所有。
在這篇由多部分組成的文章中,接下來將介紹函數(shù)式編程的一些概念,這些概念對你學(xué)習(xí)函數(shù)式編程有所幫助。如果你已經(jīng)懂了什么是函數(shù)式編程,這可以加深你的理解。
請不要著急。從這一點開始,花點時間閱讀并理解代碼示例。你甚至可能想在每節(jié)課結(jié)束后停止閱讀,以便讓你的觀點深入理解,然后再回來完成。
最重要的是你要理解。
純函數(shù)(Purity)
所謂純函數(shù),就是指這樣一個函數(shù),對于相同的輸入,永遠(yuǎn)得到相同的輸出,它不依賴外部環(huán)境,也不會改變外部環(huán)境。如果不滿足以上幾個條件那就是非純函數(shù)。
下面是Javascript中的一個純函數(shù)示例:
var z = 10; function add(x, y) {return x + y; }注意,add 函數(shù)不涉及z變量。它不從z讀取,也不從z寫入,它只讀取x和y,然后返回它們相加的結(jié)果。這是一個純函數(shù)。如果 add 函數(shù)確實訪問了變量z,那么它就不再是純函數(shù)了。
請思考一下下面這個函數(shù):
function justTen() {return 10; }如果函數(shù)justTen是純的,那么它只能返回一個常量, 為什么?
因為我們沒有給它任何參數(shù)。 而且,既然是純函數(shù)的,除了自己的輸入之外它不能訪問任何東西,它唯一可以返回的就是常量。
由于不帶參數(shù)的純函數(shù)不起作用,所以它們不是很有用。所以justTen被定義為一個常數(shù)會更好。
大多數(shù)有用的純函數(shù)必須至少帶一個參數(shù)。
考慮一下這個函數(shù):
function addNoReturn(x, y) {var z = x + y }注意這個函數(shù)是不返回任何值。它只是把變量x和y相加賦給變量z,但并沒有返回。
這個也是一個純函數(shù),因為它只處理輸入。它確實對輸入的變量進(jìn)行操作,但是由于它不返回結(jié)果,所以它是無用的。
所有有用的純函數(shù)都必須返回一些我們期望的結(jié)果。
讓我們再次考慮第一個add函數(shù):
注意 add(1, 2) 的返回結(jié)果總是 3。這不是奇怪的事情,只是因為 add 函數(shù)是純的。如果 add 函數(shù)使用了一些外部值,那么你永遠(yuǎn)無法預(yù)測它的行為。
在給定相同輸入的情況下,純函數(shù)總是返回相同的結(jié)果。
由于純函數(shù)不能改變?nèi)魏瓮獠孔兞?#xff0c;所以下面的函數(shù)都不是純函數(shù):
writeFile(fileName); updateDatabaseTable(sqlCmd); sendAjaxRequest(ajaxRequest); openSocket(ipAddress);所有這些功能都有副作用。當(dāng)你調(diào)用它們時,它們會更改文件和數(shù)據(jù)庫表、將數(shù)據(jù)發(fā)送到服務(wù)器或調(diào)用操作系統(tǒng)以獲取套接字。它們不僅對輸入操作同時也對輸出進(jìn)行操作,因此,你永遠(yuǎn)無法預(yù)測這些函數(shù)將返回什么。
純函數(shù)沒有副作用。
在Javascript、Java 和 c# 等命令式編程語言中,副作用無處不在。這使得調(diào)試非常困難,因為變量可以在程序的任何地方更改。所以,當(dāng)你有一個錯誤,因為一個變量在錯誤的時間被更改為錯誤的值,這不是很好。
此時,你可能會想,“我怎么可能只使用純函數(shù)呢?”
函數(shù)式編程不能消除副作用,只能限制副作用。由于程序必須與真實環(huán)境相連接,所以每個程序的某些部分肯定是不純的。函數(shù)式編程的目標(biāo)是盡量寫更多的純函數(shù),并將其與程序的其他部分隔離開來。
不可變性 (Immutability)
你還記得你第一次看到下面的代碼是什么時候嗎?
var x = 1; x = x + 1;教你初中數(shù)學(xué)的老師看到以上代碼,可能會問你,你忘記我給你教的數(shù)學(xué)了嗎? 因為在數(shù)學(xué)中,x 永遠(yuǎn)不能等于x + 1。
但在命令式編程中,它的意思是,取x的當(dāng)前值加1,然后把結(jié)果放回x中。
在函數(shù)式編程中,x = x + 1是非法的。所以這里你可以用數(shù)學(xué)的邏輯還記得在數(shù)式編程中這樣寫是不對的!
函數(shù)式編程中沒有變量。
由于歷史原因,存儲值的變量仍然被稱為變量,但它們是常量,也就是說,一旦x取值,這個常量就是x返回的值。別擔(dān)心,x 通常是一個局部變量,所以它的生命周期通常很短。但只要它還沒被銷毀,它的值就永遠(yuǎn)不會改變。
下面是Elm中的常量變量示例,Elm是一種用于Web開發(fā)的純函數(shù)式編程語言:
addOneToSum y z =letx = 1inx + y + z如果你不熟悉ml風(fēng)格的語法,讓我解釋一下。addOneToSum 是一個函數(shù),有兩個參數(shù)分別為y和z。
在let塊中,x被綁定到1的值上,也就是說,它在函數(shù)的生命周期內(nèi)都等于1。當(dāng)函數(shù)退出時,它的生命周期結(jié)束,或者更準(zhǔn)確地說,當(dāng)let塊被求值時,它的生命周期就結(jié)束了。
在in塊中,計算可以包含在let塊中定義的值,即 x,返回計算結(jié)果 x + y + z,或者更準(zhǔn)確地說,返回 1 + y + z,因為 x = 1。
你可能又會想 :“我怎么能在沒有變量的情況下做任何事情呢?”
我們想一下什么時候需要修改變量。通常會想到兩種情況:多值更改(例如修改或記錄對象中的單個值)和單值更改(例如循環(huán)計數(shù)器)。
函數(shù)式編程使用參數(shù)保存狀態(tài),最好的例子就是遞歸。是的,是沒有循環(huán)。“什么沒有變量,現(xiàn)在又沒有循環(huán)? ”我討厭你! ! !”
哈哈,這并不是說我們不能做循環(huán),只是沒有特定的循環(huán)結(jié)構(gòu),比如for, while, do, repeat等等。
函數(shù)式編程使用遞歸進(jìn)行循環(huán)。
這里有兩種方法可以在Javascript中執(zhí)行循環(huán):
注意,遞歸是一種函數(shù)式方法,它通過使用一個結(jié)束條件 start (start + 1) 和調(diào)用自己 accumulator (acc + start) 來實現(xiàn)與 for 循環(huán)相同的功能。它不會修改舊的值。相反,它使用從舊值計算的新值。
不幸的是,這在 Javascript中 很難想懂,需要你花點時間研究它,原因有二。第一,Javascript的語法相對其它高級語言比較亂,其次,你可能還不習(xí)慣遞歸思維。
在Elm,它更容易閱讀,如下:
sumRange start end acc =if start > end thenaccelsesumRange (start + 1) end (acc + start)它是這樣運行的:
你可能認(rèn)為 for 循環(huán)更容易理解。雖然這是有爭議的,而且更可能是一個熟悉的問題,但非遞歸循環(huán)需要可變性,這是不好的。
在這里,我還沒有完全解釋不變性的好處,但是請查看全局可變狀態(tài)部分,即為什么程序員需要限制來了解更多。
我還沒有完全解釋不可變性(Immutability)在這里的好處,但請查看 為什么程序員需要限制的全局可變狀態(tài)部分 以了解更多信息。
不可變性的好處是,你讀取訪問程序中的某個值,但只有讀權(quán)限的,這意味著不用害怕其他人更改該值使自己讀取到的值是錯誤。
不可變性的還有一個好處是,如果你的程序是多線程的,那么就沒有其他線程可以更改你線程中的值,因為該值是不可變,所以另一個線程想要更改它,它只能從舊線程創(chuàng)建一個新值。
不變性可以創(chuàng)建更簡單、更安全的代碼。
重構(gòu)
讓我們考慮一下重構(gòu),下面是一些Javascript代碼:
我們以前可能都寫過這樣的代碼,隨著時間的推移,開始意識到這兩個函數(shù)實際上是相同的,函數(shù)名稱,打印結(jié)果不太一樣而已。
我們不應(yīng)該復(fù)制 validateSsn 來創(chuàng)建 validatePhone,而是應(yīng)該創(chuàng)建一個函數(shù)(共同的部分),通過參數(shù)形式實現(xiàn)我們想要的結(jié)果。
重構(gòu)后的代碼如下:
舊代碼參數(shù)中 ssn 和 phone 現(xiàn)在用 value 表示,正則表達(dá)式 /^\d{3}-\d{2}-\d{4}$/ and /^(\d{3})\d{3}-\d{4}$/ 由變量 regex. 表示。最后,消息**“SSN”**和 “電話號碼” 由變量 type 表示。
這個有類似的函數(shù)都可以使用這個函數(shù)來實現(xiàn),這樣可以保持代碼的整潔和可維護(hù)性。
高階函數(shù)
許多語言不支持將函數(shù)作為參數(shù)傳遞,有些會支持但并不容易。
在函數(shù)式編程中,函數(shù)是一級公民。換句話說,函數(shù)通常是另一個函數(shù)的值。
由于函數(shù)只是值,我們可以將它們作為參數(shù)傳遞。即使Javascript不是純函數(shù)語言,也可以使用它進(jìn)行一些功能性的操作。 所以這里將上面的兩個函數(shù)重構(gòu)為單個函數(shù),方法是將驗證合法性的函數(shù)作為函數(shù) parseFunc 的參數(shù):
function validateValueWithFunc(value, parseFunc, type) {if (parseFunc(value))console.log('Invalid ' + type);elseconsole.log('Valid ' + type); }像函數(shù) parseFunc 接收一個或多個函數(shù)作為輸入的函數(shù),稱為 高階函數(shù)。
高階函數(shù)要么接受函數(shù)作為參數(shù),要么返回函數(shù),要么兩者兼而有之。
現(xiàn)在可以調(diào)用高階函數(shù)(這在Javascript中有效,因為Regex.exec在找到匹配時返回一個truthy值):
validateValueWithFunc('123-45-6789', /^\d{3}-\d{2}-\d{4}$/.exec, 'SSN'); validateValueWithFunc('(123)456-7890', /^\(\d{3}\)\d{3}-\d{4}$/.exec, 'Phone'); validateValueWithFunc('123 Main St.', parseAddress, 'Address'); validateValueWithFunc('Joe Mama', parseName, 'Name');這比有四個幾乎相同的函數(shù)要好得多。
但是請注意正則表達(dá)式,這里有點冗長了。簡化一下:
var parseSsn = /^\d{3}-\d{2}-\d{4}$/.exec; var parsePhone = /^\(\d{3}\)\d{3}-\d{4}$/.exec; validateValueWithFunc('123-45-6789', parseSsn, 'SSN'); validateValueWithFunc('(123)456-7890', parsePhone, 'Phone'); validateValueWithFunc('123 Main St.', parseAddress, 'Address'); validateValueWithFunc('Joe Mama', parseName, 'Name');現(xiàn)在看起來好多了。現(xiàn)在,當(dāng)要驗證一個電話號碼時,不需要復(fù)制和粘貼正則表達(dá)式了。
但是假設(shè)我們有更多的正則表達(dá)式需要解析,而不僅僅是 parseSsn 和 parsePhone。每次創(chuàng)建正則表達(dá)式解析器時,我們都必須記住在末尾添加 .exec,這很容易被忘記。
可以通過創(chuàng)建一個返回exec 的高階函數(shù)來防止這種情況:
function makeRegexParser(regex) {return regex.exec; } var parseSsn = makeRegexParser(/^\d{3}-\d{2}-\d{4}$/); var parsePhone = makeRegexParser(/^\(\d{3}\)\d{3}-\d{4}$/); validateValueWithFunc('123-45-6789', parseSsn, 'SSN'); validateValueWithFunc('(123)456-7890', parsePhone, 'Phone'); validateValueWithFunc('123 Main St.', parseAddress, 'Address'); validateValueWithFunc('Joe Mama', parseName, 'Name');這里,makeRegexParser采用正則表達(dá)式并返回exec函數(shù),該函數(shù)接受一個字符串。validateValueWithFunc 將字符串 value 傳遞給 parse 函數(shù),即exec。
parseSsn 和 parsePhone 實際上與以前一樣,是正則表達(dá)式的 exec 函數(shù)。
當(dāng)然,這是一個微小的改進(jìn),但是這里給出了一個返回函數(shù)的高階函數(shù)示例。但是,如果makeRegexParser 要復(fù)雜得多,這種更改的好處是很大的。
下面是另一個返回函數(shù)的高階函數(shù)示例:
function makeAdder(constantValue) {return function adder(value) {return constantValue + value;}; }函數(shù) makeAdder,接受參數(shù) constantValue 并返回函數(shù) adder,這個函數(shù)返回 constantValue 與它傳入?yún)?shù)相加結(jié)果。
下面是它的用法:
var add10 = makeAdder(10); console.log(add10(20)); // 打印 30 console.log(add10(30)); // 打印 40 console.log(add10(40)); // 打印 50我們通過將常量10傳遞給 makeAdder 來創(chuàng)建一個函數(shù) add10, makeAdder 返回一個函數(shù),該函數(shù)將向返回的結(jié)果都加 10。
注意,即使在 makeAddr 返回之后,函數(shù) adder 也可以訪問變量 constantValue。 這里能訪問到 constantValue 是因為存在閉包。
閉包機制非常重要,因為如果沒有它 ,返回函數(shù)的函數(shù)就不會有很大作用。所以必須了解它們是如何工作。
閉包
下面是一個使用閉包的函數(shù)的示例:
function grandParent(g1, g2) {var g3 = 3;return function parent(p1, p2) {var p3 = 33;return function child(c1, c2) {var c3 = 333;return g1 + g2 + g3 + p1 + p2 + p3 + c1 + c2 + c3;};}; }在這個例子中,child 函數(shù)可以訪問它自身的變量,函數(shù) parent 函數(shù)可以訪問它的自身變量和函數(shù) grandParent 的變量。而函數(shù) grandParent 只能訪問自身的變量。
下面是它的一個使用例子:
var parentFunc = grandParent(1, 2); // returns parent() var childFunc = parentFunc(11, 22); // returns child() console.log(childFunc(111, 222)); // prints 738 // 1 + 2 + 3 + 11 + 22 + 33 + 111 + 222 + 333 == 738在這里,parentFunc 保留了 parent 的作用域,因為 grandParent 返回 parent。
類似地,childFunc 保留了 child 的作用域,因為 parentFunc 保留了 parent 的作用域,而 parent 的作用域 保留了child 的作用域。
當(dāng)一個函數(shù)被創(chuàng)建時,它在創(chuàng)建時作用域中的所有變量在函數(shù)的生命周期內(nèi)都是可訪問的。一個函數(shù)只要還有對它的引用就存在。例如,只要childFunc 還引用 child 的作用域,child 的作用域就存在。
閉包具體還看看之前整理的一篇文章:我從來不理解JavaScript閉包,直到有人這樣向我解釋它...
原文: 1、https://medium.com/@cscalfani... 2、https://medium.com/@cscalfani...
編輯中可能存在的bug沒法實時知道,事后為了解決這些bug,花了大量的時間進(jìn)行l(wèi)og 調(diào)試,這邊順便給大家推薦一個好用的BUG監(jiān)控工具Fundebug。
你的點贊是我持續(xù)分享好東西的動力,歡迎點贊!
一個笨笨的碼農(nóng),我的世界只能終身學(xué)習(xí)!
更多內(nèi)容請關(guān)注公眾號《大遷世界》! https://blog.fundebug.com/2018/12/27/to-be-a-functional-programmer-part-1/
轉(zhuǎn)載于:https://my.oschina.net/u/3375885/blog/2993937
總結(jié)
以上是生活随笔為你收集整理的学会JavaScript函数式编程(第1部分)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 北京阿里云ACE态势感知大屏体验黑客攻击
- 下一篇: 【更新】Essential Studio