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. 這裡的數據僅僅是一個猜測。我沒有真實數據支持這一點。
關於本文
![](https://pic.pimg.tw/zzuyanan/1488615166-1259157397.png)
![](https://pic.pimg.tw/zzuyanan/1482887990-2595557020.jpg)
※徵集《前端架構設計》書評
※前端開發轉型產品經理,靠譜嗎?
※從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查詢語句