當前位置:
首頁 > 科技 > JavaScript:少一點條件語句

JavaScript:少一點條件語句

JavaScript:少一點條件語句



前言


今日早讀文章由@曬太陽的魚翻譯授權分享。


正文從這開始~

本文是《寫出複雜度低的javascript》的第三部分。在之前的文章中,我們表明了縮進(非常粗魯地)增加了代碼複雜性。雖然這不是一個準確全面的指導,至少算是一篇有幫助的指南。接著我們介紹了如何用更高級的抽象來代替循環《無循環的JavaScript》。在本文,我們將注意力轉向條件語句。


不幸的是我們無法徹底擺脫條件語句。它意味著徹底重新構建大多數代碼庫。(儘管技術上是可行的)。但是我們可以改變自身書寫條件語句的方式,從而降低代碼複雜度。接下來我們介紹處理if語句的兩種策略。在那之後,我們將把注意力轉向switch語句。


沒有else的 if語句


第一種重構條件語句的方法是避免使用else。在JavaScript中,我們可以當做沒有Else語句來編寫我們的代碼。 這件事可能看來很奇怪。實際上在大多數情況下,我們確實不需要else語句。


想像一下,我們正在開發一個給科學家研究發光體的網站。每一個科學家有一個通過AJAX載入的通知菜單。當數據載入完畢,我們有一些用於顯示菜單的代碼。


functionrenderMenu(menuData){


letmenuHTML= ;


if((menuData===null)||(!Array.isArray(menuData)){


menuHTML=


Most profuse apologies. Our server seems to have failed in it』s duties

;


}elseif(menuData.length===){


menuHTML=


No new notifications


;


}else{


menuHTML=


+menuData.map((item)=>`${item.content}`).join( )


+ ;


}

returnmenuHTML;


}


上面這段代碼可以正常工作。但是一旦我們確定沒有什麼通知要呈現,條件的終止點又是哪裡?為何不選擇直接返回menuHTML這個值?讓我們重構並看看代碼是什麼樣子。


functionrenderMenu(menuData){


if((menuData===null)||(!Array.isArray(menuData)){


return


Most profuse apologies. Our server seems to have failed in it』s duties


;


}


if(menuData.length===){

return


No new notifications


;


}


return


+menuData.map((item)=>`${item.content}`).join( )


+ ;


}


如上,我們把代碼修改成一旦我們符合一個條件,我們直接返回結果然後退出。對讀者來說,如果這個條件是你所關心的,那麼沒有必要閱讀之後的代碼了。因為我們知道在If語句後不會再有相關代碼,所以不需要進一步閱讀與檢驗了。


這段代碼另一個好處就是能夠降低主路徑(返回一個list)的縮進級別。這使得我們能夠更加容易看出該代碼預期的通常路徑。If語句用來處理主路徑上的異常,這樣讓我們的代碼意圖顯得更加清晰。

這種不使用else的策略是另一個大策略的子集。這個大策略被我稱為「早點返回,經常返回「(return early,return often)。一般來說,這個策略能夠讓代碼更清晰並且有時能夠減少計算。比如我們在之前文章中提過的find 函數。


functionfind(predicate,arr){


for(letitemofarr){


if(predicate(item)){


returnitem;


}


}


}


在這個find函數內,一旦我們找到想查找的對象就立刻返回該對象並且退出循環。這使得我們的代碼更加高效。


「早點返回,經常返回」

移除else是一個好的開始,但仍然讓代碼有很多縮進。一個稍微更好的策略就是擁抱三元操作符。


不要懼怕三元操作符


三元操作符一直有著讓代碼更難讀的壞名聲。在介紹之前,我想聲明你永遠不應該嵌套使用三元操作符。嵌套使用三元操作符會使代碼難以置信地難讀【1】


。但是三元操作符比平常的if語句有巨大的優勢。為了說明這一點,我們必須深入研究if語句到底做了什麼。我們來看一個例子:


letfoo;


if(bar=== some value ){


foo=baz;


}


else{


foo=bar;

}


這代碼非常簡單易讀。但是如果我們把塊包裝在里立刻執行函數表達式(IIFE)里會怎麼樣呢?


letfoo;


if(bar=== some value )(function(){


foo=baz;


}())


else(function(){


foo=qux;


}());


截至目前,我們實際上沒有任何改變,兩個代碼例子做的都是同一樣事情。但是請注意,所有的IFFE沒有返回值。這意味著它們是不純的(impure)。這種結果是理所應當的,因為我們僅僅只是原封不動地複製了原來的if語句。但是我們是否可以將這些IIFE重構為純函數?答案是不能。至少不能做到每一個代碼塊一個函數。 有一條提議可以改變這種情況。但是現在我們必須接受,除非我們早點返回結果,否則if語句本身就是不純的事實。為了實現任何有用的邏輯,我們不得不改變一些變數或者在這些分支代碼塊中造成副作用。除非我們早點返回結果。

但是,假如我們把整個if語句包裹在一個函數內呢?我們能否讓這個包裝函數變成純函數?我們來試一下。首先我們把整個if語句包裹在一個IIFE 里:


letfoo=null;


(function(){


if(bar=== some value ){


foo=baz;


}


else{


foo=qux;


}


})();

接著我們改動一下讓我們能夠從IIFE中返回值。


letfoo=(function(){


if(bar=== some value ){


returnbaz;


}


else{


returnqux;


}


})();


這個改進在於我們不再需要改變任何變數。代碼中,我們的IIFE不知道foo變數的存在。但是它通過作用域鏈在訪問bar,baz和qux變數。讓我們先處理baz與qux變數。我們把它們變成函數的入參(注意最後一行)。


letfoo=(function(returnForTrue,returnForFalse){


if(bar=== some value ){


returnreturnForTrue;


}


else{


returnreturnForFalse;


}


})(baz,qux);


最終,我們需要處理一下bar。我們可以把它當做變數處理,但是那樣做的話會導致只能處理與一些值的比較。假如我們把整個條件當做一個函數入參處理,函數就能變得更加靈活。


letfoo=(function(returnForTrue,returnForFalse,condition){


if(condition){


returnreturnForTrue;


}


else{


returnreturnForFalse;


}


})(baz,qux,(bar=== some value ));


現在我們可以把整個函數提取出來(記得避免使用else)。


functionconditional(returnForTrue,returnForFalse,condition){


if(condition){


returnreturnForTrue;


}


returnreturnForFalse;


}


letfoo=conditional(baz,qux,(bar=== some value )


截至目前,我們做了什麼?我們已經抽象設置值的if語句。如果我們想要,我們都能像這樣改造所有的if語句,只要這些if語句都是用來設置一個值。作為結果,我們用一個純函數調用代替了到處都是的if語句。我們將刪除一堆縮進並且改進了代碼。


但是..我們實際上不需要conditional這個函數。我們已經有了三元操作符來實現同樣的功能。


letfoo=(bar=== some value )?baz:qux;


三元操作符是簡潔的,內置於語言中。我們不需要再編寫或導入特殊的函數獲得所有相同的優勢。三元操作符真正唯一的缺點就是你實際上不能對三元操作符使用curry() 和 compose()【2】。所以請給三元操作符一個機會。看看你是否能夠用三元操作符來重構你的if語句。至少你能獲得如何構建代碼的新視角


切換switch


JavaScript有另一種條件結構,如同if語句。Switch語句就是另一種引入縮進的控制結構,並且使用它會帶來複雜性。稍後我們將會看看如果編寫沒有switch語句的代碼。但是首先,我想說幾句關於它的優點。


Switch語句是我們在JavaScript最接近模式匹配(pattern matching)的【3】。模式匹配是一個好事。計算機科學技術建議我們用模式匹配來代替if語句。因此,我們可以很好地使用switch語句。


Switch語句同時允許定義針對多種情況的單一響應。再次強調,這有點像其他語言中的模式匹配。在某些情況下,這種用法非常方便。所以再說一遍,switch語句並不總是壞的。


在許多情況下我們應該重構switch語句。我們來看看一個例子。回想一下我們之前提到的發光體的論壇例子。假設我們有三種類型的通知。一個科學家可能收到通知當他:


有人引用了他們發表的論文


有人開始加入他們的工作


有人在一篇文章中提到他們


我們有不同的圖標與文字格式來顯示每一種通知。


letnotificationPtrn;


switch(notification.type){


case citation :


notificationPtrn= You received a citation from {}. ;


break;


case follow :


notificationPtrn= {} started following your work ;


break;


case mention :


notificationPtrn= {} mentioned you in a post. ;


break;


default:


// Well, this should never happen


}


// Do something with notificationPtrn


有一件事情讓switch語句變的有點惹人厭,那就是太容易忘記寫break了。但是如果我們把switch包裹在一個函數內,我們就能使用之前提過的「早點返回,經常返回」的策略。這代表我們可以擺脫break語句的困擾。


functiongetnotificationPtrn(n){


switch(n.type){


case citation :


return You received a citation from {}. ;


case follow :


return {} started following your work ;


case mention :


return {} mentioned you in a post. ;


default:


// Well, this should never happen


}


}


letnotificationPtrn=getNotificationPtrn(notification);


這樣寫就好多了。我們現在有一個純函數來代替修改變數。但是我們可以使用簡單JavaScript對象(POJO)來實現同樣的結果。


functiongetNotificationPtrn(n){


consttextOptions={


citation: You received a citation from {}. ,


follow: {} started following your work ,


mention: {} mentioned you in a post. ,


}


returntextOptions[n.type];


}


這個版本能夠實現之前的一樣的效果。代碼變的更加緊湊。但它是否更加簡單?


我們所做的事就是用數據代替一種控制結構。這比我們想像中的更有意義。現在我們能夠把textOptions作為getNotification的函數入參。例如:


consttextOptions={


citation: You received a citation from {}. ,


follow: {} started following your work ,


mention: {} mentioned you in a post. ,


}


functiongetNotificationPtrn(txtOptions,n){


returntxtOptions[n.type];


}


constnotificationPtrn=getNotificationPtrn(txtOptions,notification);


起初這段代碼看起來不太有趣。但是現在思考一下,textOptions是一個變數。並且該變數不用再被硬編碼了。我們可以把它移動到一個JSON配置文件中,或者從伺服器獲取該變數。現在我們想怎麼改textOptions就怎麼改。我們可以增加新的選項,或者移除選項。我們也可以把多個地方不同的選項合并到一起。同時這個版本有更少的代碼縮進。


你可能注意到了,這個版本我們沒有代碼處理未知的通知類型。如果用switch語句,我們可以用default實現處理未知的選項。當遭遇未知的類型時,我們可以在default中拋出未知錯誤,或者返回一段有意義的消息給用戶。例如:


functiongetNotificationPtrn(n){


switch(n.type){


case citation :


return You received a citation from {}. ;


case follow :


return {} started following your work ;


case mention :


return {} mentioned you in a post. ;


default:


thrownewError( You』ve received some sort of notification we don』t know about. ;


}


}


現在我們能夠處理未知的通知類型了。但是我們又再次了使用switch語句。我們能否在POJO的方式中處理類似的情況?


一種選項就是使用if 語句:


functiongetNotificationPtrn(txtOptions,n){


if(typeoftxtOptions[n.type]=== undefined ){


return You』ve received some sort of notification we don』t know about. ;


}


returntxtOptions[n.type];


}


但是我們之前說了,我們要嘗試去除我們的if語句。所以這種選項並不理想。幸運的是,我們可以利用JavaScript中的弱類型優勢結合布爾邏輯來實現我們的目標。在||運算符中,如果第一部分是假值,JavaScript會直接返回第二部分。如果我們傳遞了未知的通知類型給對象,對象會返回一個undefined結果。在JavaScript中會將undefined作為假值。所以我們可以像這樣利用or 表達式:


functiongetNotificationPtrn(txtOptions,n){


returntxtOptions[n.type]


|| You』ve received some sort of notification we don』t know about. ;


}


我們也可以把默認的信息作為參數。


constdflt= You』ve received some sort of notification we don』t know about. ;


functiongetNotificationPtrn(defaultTxt,txtOptions,n){


returntxtOptions[n.type]||defaultTxt;


}


constnotificationPtrn=getNotificationPtrn(defaultTxt,txtOptions,notification.type);


看看現在這個版本,是否比switch版本好多了?答案像平常一樣,「看情況」。有些人可能會認為這個版本對於初學者難以閱讀。這個問題客觀存在。為了理解這個版本,你不得不要學習JavaScript如何強制轉化變數為布爾類型。但是這個觀點問的問題實際上是:「難道這個問題是因為它很複雜,還是因為它不熟悉」。因為不熟悉所以我們要接受更複雜的代碼?


但是這段代碼難道不是更簡單了?讓我們看看最新的函數。如果我們把函數取一個更通用的名字(並調整了最後的參數),看看現在是怎麼樣的?


functionoptionOrDefault(defaultOption,optionsObject,switchValue){


returnoptionsObject[switchValue]||defaultOption;


}


我們可以創建像下面一樣創建getNotificationPtrn 函數。


constdflt= You』ve received some sort of notification we don』t know about. ;


consttextOptions={


citation: You received a citation from {}. ,


follow: {} started following your work ,


mention: {} mentioned you in a post. ,


}


functiongetNotificationPtrn(notification){


returnoptionOrDefault(dflt,textOptions,notification.type);


}


現在我們有了一個非常清晰的關注點分離(separation of concerns)。文字選項與默認消息現在都是純數據。它們不在被包含在控制結構中。同時我們有一個便利的函數,optionOrDefault,用於處理類似的情況。數據與選擇要顯示哪個選項的任務完全分離。


這種模式對於處理返回靜態數值的情況非常方便。根據我的經驗,可以在60~70%的情況中取代switch語句[4]。但是如果我們想做一些更加有趣的事情?想像一下,如果我們選項中包含了函數而不是字元串?本文已經太長了,所以我們不會再詳細討論這種情況。不過這種情況值得我們思考。


像往常一樣,小心地使用你的大腦。optionOrDefault函數可以替代許多switch語句。但不是所有的情況都能這樣代替。某些情況下,使用switch更加合理。


總結


重構條件比刪除循環需要更多的精力。因為我們以不同的方式使用條件語句,而循環通常配合數組一起使用。但是我們可以應用一些簡單的模式來讓條件減少交叉。它們包括:「早點返回,經常返回「(return early,return often)、「使用三元操作符」、「用對象代替switch語句」。這些都不是萬能銀彈,而是打擊複雜度的利器。


1. Joel Thom 不同意這個觀點。不過我們都主張用三元操作符代替if語句


2. Curry 和compose 都是我們在函數式編程大量使用的工具。如果你沒有接觸過這些,可以閱讀這篇文章。特別是第三部分與第四部分。


3. 也就是說,你眯著眼看,可能覺得switch語句有點想模式匹配。不過只有你使用「早點返回」的策略才會這樣。如果你不早點返回,switch語句總是會一團糟。


4. 這裡的數據僅僅是一個猜測。我沒有真實數據支持這一點。


關於本文


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 前端早讀課 的精彩文章:

徵集《前端架構設計》書評
前端開發轉型產品經理,靠譜嗎?
從Vue的第二個commit來學習數據驅動視圖
支付寶前端構建工具的發展和未來的選擇
【圖書】Angular權威教程

TAG:前端早讀課 |

您可能感興趣

VBScript 條件語句
try-catch語句
postgresql的copy語句和備份恢復
英語口語天天練!實用口語句子匯總!What is your opinion?
Perl 條件語句
mybatis框架的動態sql語句
MySql 優化 group by 語句
Go 系列教程—10.switch 語句
學習MySQL的select語句
Scala IF...ELSE 語句
djang常用查詢SQL語句
sql語句的使用&mysql單表練習(小白專用版之二)
總是被嘲笑英語句式Chinglish?小眾高分寫作句式打包送你
Mybatis 查詢語句結果集總結
常用傻瓜式SQL Server語句,優化資料庫
initial語句中的並行執行和串列執行
一條SQL語句在MySQL中是如何執行的?
小鄭搞碼事:為什麼建議大家在JS代碼中,永遠不要使用with語句
忘了Python關鍵語句?這份備忘錄拯救你的記憶
學習資料庫要掌握的54條SQL查詢語句