JAVASCRIPT. BUT LESS IFFY

RingChenng發表於2018-11-15

“這很難,是因為它很複雜,還是因為對它不熟悉?熟悉度是接受更復雜程式碼的充分理由嗎?”

發現一篇好文,看完之後寫程式碼瞬間好理解了很多!!!翻譯一下全文。

寫不令人疑惑的 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 語句”。這些不是銀彈,而是用於對抗複雜情況的便利武器。