“這很難,是因為它很複雜,還是因為對它不熟悉?熟悉度是接受更復雜程式碼的充分理由嗎?”
發現一篇好文,看完之後寫程式碼瞬間好理解了很多!!!翻譯一下全文。
寫不令人疑惑的 JavaScript 程式碼
原文地址:jrsinclair.com/articles/20…
這是關於降低 JavaScript 程式碼複雜性的一系列文章的第三部分。在以前的文章中,我們認為縮排是複雜性的一個指標。這不是一個準確或全面的指標,但它可以成為一個有用的指南。然後,我們研究了如何用更高階別的抽象來代替迴圈。在這篇文章中,我們將注意力轉向條件句。
不幸的是,我們不能完全擺脫條件句,這將意味著對大多數程式碼庫進行徹底的重新設計(儘管技術上是可能的)。但是,我們可以改變我們寫條件句的方式,使它們不那麼複雜。我們將研究兩種處理 if 語句的策略。之後,我們將把注意力轉向轉換語句。
沒有 else 的 if 語句
重構條件句的第一種方法是去掉 else。我們寫程式碼就好像 JavaScript 裡面沒有 else 語句一樣。這似乎是一件奇怪的事情,但是大多數時候,我們根本不需要 else。
想象一下我們正在開發一個網站,我們通過AJAX載入。載入資料後,我們有一些用於呈現選單的程式碼:
function renderMenu(menuData) {
let menuHTML = '';
if ((menuData === null) || (!Array.isArray(menuData))) {
menuHTML = '<div class="menu-error">Most profuse apologies. Our server seems to have failed in it’s duties</div>';
} else if (menuData.length === 0) {
menuHTML = '<div class="menu no-notifications">No new notifications</div>';
} else {
menuHTML = `
<ul class="menu notifications">
${menuData.map((item) => `<li><a href="${item.link}">${item.content}</a></li>`).join('')}
</ul>
`;
}
return menuHTML;
}
複製程式碼
這個程式碼可以工作。但是一旦我們確定沒有要渲染的 notifications,再看其他語句有什麼意義呢?為什麼不直接 return menuHTML
呢?讓我們重構一下,看看它是什麼樣子:
function renderMenu(menuData) {
if ((menuData === null) || (!Array.isArray(menuData))) {
return '<div class="menu-error">Most profuse apologies. Our server seems to have failed in it’s duties</div>';
}
if (menuData.length === 0) {
return '<div class="menu-no-notifications">No new notifications</div>';
}
return '<ul class="menu-notifications">'
+ menuData.map((item) => `<li><a href="${item.link}">${item.content}</a></li>`).join('')
+ '</ul>';
}
複製程式碼
所以,我們已經修改了程式碼,這樣,如果我們碰到了一個 edge case
,我們就返回一些東西,然後離開那裡。對於程式碼閱讀者來說,如果你只關心這個 edge case
,就沒有必要再閱讀下去了。我們知道在 if 語句之後不可能有任何相關的程式碼。不需要小心翼翼的向下細看和檢查。
這個程式碼的另一個好處是“主”路徑(返回一個 list)已經降低了縮排級別。這使得更容易看出這是程式碼中預期的“常規”路徑。if 語句用於處理主路徑的【例外】。這使得我們程式碼的意圖更加清晰。
這種不使用 else 的策略稱之為“提前 return、總是 return”。總的來說,我發現它使程式碼更清晰,有時可以減少計算量。例如,在上一篇文章中,我們看了 find()
:
function find(predicate, arr) {
for (let item of arr) {
if (predicate(item)) {
return item;
}
}
}
複製程式碼
在 find()
函式中,我們一找到要查詢的專案,就返回退出迴圈,這使得程式碼更加高效。
- 早點返回,經常返回
- 去除 else 是一個好的開始,但是仍然會給我們留下很多縮排。更好的策略是採用三元運算子
不要害怕三元運算子
三元運算子名聲不好,說它在降低程式碼可讀性。三元運算子確實讓程式碼難以閱讀。但是,與傳統的 if 語句相比,三元運算子有著巨大的優勢。為了說明為什麼我們必須深入研究 if 語句的作用。讓我們來看一個例子:
let foo;
if (bar === 'some value') {
foo = baz;
}
else {
foo = bar;
}
複製程式碼
這很簡單。但是,如果我們將這些塊包裝在立即呼叫函式表示式( IIFEs )中會發生什麼呢?
let foo;
if (bar === 'some value') (function () {
foo = baz;
}())
else (function () {
foo = qux;
}());
複製程式碼
到目前為止,我們什麼也沒有改變,兩個程式碼示例都做了同樣的事情。但是請注意,IIFE 沒有返回任何東西。這意味著它是不純的。這是意料之中的,因為我們只是複製了原始的 if 語句。我們能把這些 IIFEs 函式重構為純函式嗎?事實上,我們不能。至少,每個塊沒有一個函式。我們不能這樣做的原因是 if 語句不返回任何內容。有人提議改變這一點,但是目前,我們必須接受這一點,除非我們早點 return,否則 if 語句也變得不純。例如需要做點什麼事,要麼存個變數,要麼在條件句裡面產生一點副作用,除非我們可以早點 return。
如果我們將一個函式包裝在整個 if 語句中會怎麼樣?我們可以讓包裝函式變得更純嗎?讓我們試試。首先,我們將整個 if 語句包裝在 IIFE 中:
let foo = null;
(function () {
if (bar === 'some value') {
foo = baz;
}
else {
foo = qux;
}
})();
複製程式碼
然後我們通過將條件語句包在一個立即執行函式中返回結果:
let foo = (function () {
if (bar === 'some value') {
return baz;
}
else {
return qux;
}
})();
複製程式碼
這是一個改進,因為我們不再改變任何變數。我們的 LIFE 對 foo 一無所知。但是它仍然在訪問其範圍之外的變數: bar、baz 和 qux。讓我們先處理 baz 和 qux。我們將使它們成為函式的引數(注意最後一行) :
let foo = (function (returnForTrue, returnForFalse) {
if (bar === 'some value') {
return returnForTrue;
}
else {
return returnForFalse;
}
})(baz, qux);
複製程式碼
最後,我們需要處理 bar。我們也可以把它作為一個變數傳入,但是我們總是把它和“某個值”相比較。如果我們將整個條件語句作為一個引數,我們可以增加一點靈活性:
let foo = (function (returnForTrue, returnForFalse, condition) {
if (condition) {
return returnForTrue;
}
else {
return returnForFalse;
}
})(baz, qux, (bar === 'some value'));
複製程式碼
現在,我們可以獨立地將我們的 function 移出(並且去掉了 else) :
function conditional(returnForTrue, returnForFalse, condition) {
if (condition) {
return returnForTrue;
}
return returnForFalse;
}
let foo = conditional(baz, qux, (bar === 'some value'));
複製程式碼
我們做了什麼?我們已經為設定值的 if 語句建立了一個抽象。如果我們願意,我們可以用這種方式重構(幾乎)所有的 if 語句,只要它們設定了一個值。因此,我們沒有到處使用 if 語句,而是使用了純函式呼叫。我們將刪除一堆縮排並改進程式碼。
但是……我們並不真正需要有 conditional()
。我們已經有了三元運算子,它執行完全相同的操作:
let foo = (bar === 'some value') ? baz : qux;
複製程式碼
三元運算子簡潔,並內建於語言中。我們不需要編寫或匯入特殊函式來獲得所有相同的能力。唯一真正的缺點是你不能真正使用curry()
和 compose()
搭配三元運算。所以,試試看。看看你是否可以用三元運算重構你的 if 語句。至少你將獲得一個關於如何構造程式碼的新視角。
移除 switches
JavaScript 還有另一個條件結構,和 if 語句一樣。switch 語句是另一種引入縮排和複雜性的控制結構。過一會兒,我們將研究如何編寫沒有 switch 的語句。但是首先,我想對他們說幾句好話:
switch 語句是 JavaScript 中最接近模式匹配的東西。模式匹配是件好事。模式匹配是電腦科學家推薦我們使用的,而不是 if語句。因此,是可以很好地使用 switch 語句的。
switch 語句還允許您定義對多種情況的單個響應。這同樣類似於其他語言中的模式匹配。在某些情況下,這可能非常方便。switch 語句也不總是不好的。
儘管這樣,但在許多情況下,我們應該重構 switch 語句。讓我們看一個例子,我們有三種不同型別的通知:
- 有人引用了他們寫的一篇論文
- 有人開始“跟蹤”他們的工作
- 有人在帖子中提到了他們
我們有不同的圖示和文字格式,每種通知有不同的顯示:
let notificationPtrn;
switch (notification.type) {
case 'citation':
notificationPtrn = 'You received a citation from {{actingUser}}.';
break;
case 'follow':
notificationPtrn = '{{actingUser}} started following your work';
break;
case 'mention':
notificationPtrn = '{{actingUser}} mentioned you in a post.';
break;
default:
// Well, this should never happen
}
// Do something with notificationPtrn
複製程式碼
switch 語句有點討厭的一件事是,忘記一次 break
太容易了。但是如果我們把它變成一個函式,我們可以使用以前的“提前 return,經常 return”技巧。這意味著我們可以擺脫 break
語句:
function getnotificationPtrn(n) {
switch (n.type) {
case 'citation':
return 'You received a citation from {{actingUser}}.';
case 'follow':
return '{{actingUser}} started following your work';
case 'mention':
return '{{actingUser}} mentioned you in a post.';
default:
// Well, this should never happen
}
}
let notificationPtrn = getNotificationPtrn(notification);
複製程式碼
這好多了。我們現在有了一個純函式,而不是改變一個變數。但是,我們也可以使用一個 plain ol' JavaScript object (POJO) 來獲得相同的結果:
function getNotificationPtrn(n) {
const textOptions = {
citation: 'You received a citation from {{actingUser}}.',
follow: '{{actingUser}} started following your work',
mention: '{{actingUser}} mentioned you in a post.',
}
return textOptions[n.type];
}
複製程式碼
這產生了與 getnotificationPtrn
相同的結果。它更緊湊。但是這更簡單嗎?
我們所做的是用資料替換控制結構。這比聽起來更重要。現在,如果我們願意,我們可以讓 TextOptions
成為 GetNotification()
的一個引數。例如:
const textOptions = {
citation: 'You received a citation from {{actingUser}}.',
follow: '{{actingUser}} started following your work',
mention: '{{actingUser}} mentioned you in a post.',
}
function getNotificationPtrn(txtOptions, n) {
return txtOptions[n.type];
}
const notificationPtrn = getNotificationPtrn(txtOptions, notification);
複製程式碼
這可能不太有趣。但是現在考慮一下,TextOptions
是一個變數。這個變數不再需要硬編碼。我們可以將它移動到 JSON 配置檔案中,或者從伺服器獲取它。如果願意,我們現在可以增加或者刪除選項。我們可以合併不同的選項。這個版本中的縮排也少得多…
但是,您可能已經注意到,這些程式碼都沒有處理我們未知型別的情況。在 switch 語句中,我們有 default 選項。如果遇到未知型別,我們可以用它來丟擲錯誤。或者我們可以向使用者返回一個的訊息。例如:
function getNotificationPtrn(n) {
switch (n.type) {
case 'citation':
return 'You received a citation from {{actingUser}}.';
case 'follow':
return '{{actingUser}} started following your work';
case 'mention':
return '{{actingUser}} mentioned you in a post.';
default:
throw new Error('You’ve received some sort of notification we don’t know about.';
}
}
複製程式碼
我們處理了未知的情況,但是我們又使用了 switch 語句。我們能在 POJO 中處理這個問題嗎?
一種選擇是使用 if 語句:
function getNotificationPtrn(txtOptions, n) {
if (typeof txtOptions[n.type] === 'undefined') {
return 'You’ve received some sort of notification we don’t know about.';
}
return txtOptions[n.type];
}
複製程式碼
但是我們正試圖減少我們的 if 語句。所以這也不理想。相反,我們將利用 JavaScript 的鬆散型別,結合一些布林邏輯。如果 OR 表示式的第一部分是錯誤的,JavaScript 將只檢查第二部分(||)。如果在物件中找不到型別,則型別將是 undefined
的。JavaScript將把 undefined
解釋為 false
。所以,我們像這樣使用OR表示式:
function getNotificationPtrn(txtOptions, n) {
return txtOptions[n.type]
|| 'You’ve received some sort of notification we don’t know about.';
}
複製程式碼
此外,我們也可以將預設值作為引數:
const defaultTxt = 'You’ve received some sort of notification we don’t know about.';
function getNotificationPtrn(defaultTxt, txtOptions, n) {
return txtOptions[n.type] || defaultTxt;
}
const notificationPtrn = getNotificationPtrn(defaultTxt, txtOptions, notification.type);
複製程式碼
現在,這種方法比 switch
語句好嗎?一如往常,答案是“這取決於...”。有些人可能會認為這個版本對於初學者來說很難閱讀。這是一個合理的擔憂。為了理解正在發生的事情,你必須瞭解 JavaScript 是如何強制值變成布林值的。但是要問的問題是,“這很難,是因為它很複雜,還是因為對它不熟悉?熟悉度是接受更復雜程式碼的充分理由嗎?“
但是可以減少程式碼的複雜度嗎?讓我們看看我們建立的最後一個函式。如果我們把它的名字改成更通用的名字(並調整最後一個引數)會怎麼樣?
function optionOrDefault(defaultOption, optionsObject, switchValue) {
return optionsObject[switchValue] || defaultOption;
}
複製程式碼
然後,我們可以像這樣構建 getNotificationPtrn
函式:
const dflt = 'You’ve received some sort of notification we don’t know about.';
const textOptions = {
citation: 'You received a citation from {{actingUser}}.',
follow: '{{actingUser}} started following your work',
mention: '{{actingUser}} mentioned you in a post.',
}
function getNotificationPtrn(notification) {
return optionOrDefault(dflt, textOptions, notification.type);
}
複製程式碼
我們現在有一個非常明確的概念。文字選項和預設訊息現在是純資料。它們不再嵌入控制結構中。我們還有一個方便的函式 optionOrDefault()
,用於構建類似型別的構造。資料與選擇顯示哪個選項的任務完全分開。
當我們處理返回靜態值時,這個模式很方便。根據我的經驗,在大約 60-70% 的情況下,它可以取代 switch 語句。但是如果我們想做一些更有趣的事情呢?想象一下,如果我們的 options 物件包含函式而不是字串,會發生什麼?這篇文章已經太長了,所以我們不在這裡深入討論細節。但是這很值得考慮。
現在,像往常一樣,小心使用你的大腦。OptionOrDefault()
這樣的函式可以替換許多 switch
語句。但不是全部。在某些情況下,使用 switch
語句更有意義。沒關係。
總結
重構條件比移除迴圈要更有用點。這部分是因為我們以許多不同的方式使用它們。然而,迴圈主要(但不總是)與陣列一起使用。但是我們可以應用一些簡單的模式來減少條件句之間的糾纏。它們包括:“提前 return”,“使用三元運算子”,以及“用物件替換 switch 語句”。這些不是銀彈,而是用於對抗複雜情況的便利武器。