「譯」編寫更好的 JavaScript 條件式和匹配條件的技巧

Chor發表於2019-06-25

介紹

如果你像我一樣樂於見到整潔的程式碼,那麼你會盡可能地減少程式碼中的條件語句。通常情況下,物件導向程式設計讓我們得以避免條件式,並代之以繼承和多型。我認為我們應當儘可能地遵循這些原則。

正如我在另一篇文章 JavaScript 整潔程式碼的最佳實踐裡提到的,你寫的程式碼不單單是給機器看的,還是給“未來的自己”以及“其他人”看的。

從另一方面來說,由於各式各樣的原因,可能我們的程式碼最終還是會有條件式。也許是修復 bug 的時間很緊,也許是不使用條件語句會對我們的程式碼庫造成大的改動,等等。本文將會解決這些問題,同時幫助你組織所用的條件語句。

技巧

以下是關於如何構造 if...else 語句以及如何用更少的程式碼實現更多功能的技巧。閱讀愉快!

1. 要事第一。小細節,但很重要

不要使用否定條件式(這可能會讓人感到疑惑)。同時,使用條件式簡寫來表示 boolean 值。這個無須再強調了,尤其是否定條件式,這不符合正常的思維方式。

不好的:

const isEmailNotVerified = (email) => {
    // 實現
}

if (!isEmailNotVerified(email)) {
    // 做一些事...
}

if (isVerified === true) {
    // 做一些事...
}

好的:

const isEmailVerified = (email) => {
    // 實現
}

if (isEmailVerified(email)) {
    // 做一些事...
}

if (isVerified) {
    // 做一些事...
}

現在,理清了上面的事情後,我們就可以開始了。

2. 對於多個條件,使用 Array.includes

假設我們想要在函式中檢查汽車模型是 renault 還是 peugeot。那麼程式碼可能是這樣的:

const checkCarModel = (model) => {
    if(model === 'renault' || model === 'peugeot') { 
    console.log('model valid');
    }
}

checkCarModel('renault'); // 輸出 'model valid'

考慮到我們只有兩個模型,這麼做似乎也還能接受,但如果我們還想要檢查另一個或者是幾個模型呢?如果我們增加更多 or 語句,那麼程式碼將變得難以維護,且不夠整潔。為了讓它更加簡潔,我們可以像這樣重寫函式:

const checkCarModel = (model) => {
    if(['peugeot', 'renault'].includes(model)) { 
    console.log('model valid');
    }
}

checkCarModel('renault'); // 輸出 'model valid'

上面的程式碼看起來已經很漂亮了。為了更進一步改善它,我們可以建立一個變數來存放汽車模型:

const checkCarModel = (model) => {
    const models = ['peugeot', 'renault'];

    if(models.includes(model)) { 
    console.log('model valid');
    }
}

checkCarModel('renault'); // 輸出 'model valid'

現在,如果我們想要檢查更多模型,只需要新增一個新的陣列元素即可。此外,如果它很重要的話,我們還可以將 models 變數定義在函式作用域外,並在需要的地方重用。這種方式可以讓我們集中管理,並使維護變得輕而易舉,因為我們只需在程式碼中更改一個位置。

3. 匹配所有條件,使用 Array.every 或者 Array.find

在本例中,我們想要檢查每個汽車模型是否都是傳入函式的那一個。為了以更加命令式的方式實現,我們會這麼做:

const cars = [
  { model: 'renault', year: 1956 },
  { model: 'peugeot', year: 1968 },
  { model: 'ford', year: 1977 }
];

const checkEveryModel = (model) => {
  let isValid = true;

  for (let car of cars) {
    if (!isValid) {
      break;
    }
    isValid = car.model === model;
  }

  return isValid;
}

console.log(checkEveryModel('renault')); // 輸出 false

如果你更喜歡以命令式的風格行事,上面的程式碼或許還不錯。另一方面,如果你不關心其背後發生了什麼,那麼你可以重寫上面的函式並使用 Array.every 或者 Array.find 來達到相同的結果。

const checkEveryModel = (model) => {
  return cars.every(car => car.model === model);
}

console.log(checkEveryModel('renault')); // 輸出 false

通過使用 Array.find 並做輕微的調整,我們可以達到相同的結果。兩者的表現是一致的,因為兩個函式都為陣列中的每一個元素執行了回撥,並且在找到一個 falsy 項時立即返回 false

const checkEveryModel = (model) => {
  return cars.find(car => car.model !== model) === undefined;
}

console.log(checkEveryModel('renault')); // 輸出 false

4. 匹配部分條件,使用 Array.some

Array.every 匹配所有條件,這個方法則可以輕鬆地檢查我們的陣列是否包含某一個或某幾個元素。為此,我們需要提供一個回撥並基於條件返回一個布林值。

我們可以通過編寫一個類似的 for...loop 語句來實現相同的結果,就像之前寫的一樣。但幸運的是,有很酷的 JavaScript 函式可以來幫助我們完成這件事。

const cars = [
  { model: 'renault', year: 1956 },
  { model: 'peugeot', year: 1968 },
  { model: 'ford', year: 1977 }
];

const checkForAnyModel = (model) => {
  return cars.some(car => car.model === model);
}

console.log(checkForAnyModel('renault')); // 輸出 true

5. 提前返回而不是使用 if...else 分支

當我還是學生的時候,就有人教過我:一個函式應該只有一個返回語句,並且只從一個地方返回。如果細心處理,這個方法倒也還好。我這麼說也就意味著,我們應該意識到它在某些情況下可能會引起條件式巢狀地獄。如果不受控制,多個分支和 if...else 巢狀將會讓我們感到很痛苦。

另一方面,如果程式碼庫很大且包含很多行程式碼,位於深層的一個返回語句可能會帶來問題。現在我們都實行關注點分離和 SOLID 原則,因此,程式碼行過多這種情況挺罕見的。

舉例來解釋這個問題。假設我們想要顯示所給車輛的模型和生產年份:

const checkModel = (car) => {
  let result; // 首先,定義一個 result 變數
  
  // 檢查是否有車
  if(car) {

    // 檢查是否有車的模型
    if (car.model) {

      // 檢查是否有車的年份
      if(car.year) {
        result = `Car model: ${car.model}; Manufacturing year: ${car.year};`;
      } else {
        result = 'No car year';
      }
    
    } else {
      result = 'No car model'
    }   

  } else {
    result = 'No car';
  }

  return result; // 我們的單獨的返回語句
}

console.log(checkModel()); // 輸出 'No car'
console.log(checkModel({ year: 1988 })); // 輸出 'No car model'
console.log(checkModel({ model: 'ford' })); // 輸出 'No car year'
console.log(checkModel({ model: 'ford', year: 1988 })); // 輸出 'Car model: ford; Manufacturing year: 1988;'

正如你所看到的,即使本例的問題很簡單,上面的程式碼也實在太長了。可以想象一下,如果我們有更加複雜的邏輯會發生什麼事。大量的 if...else 語句。

我們可以重構上面的函式,分解成多個步驟並稍做改善。例如,使用三元操作符,包括 && 條件式等。不過,這裡我直接跳到最後,向你展示藉助現代 JavaScript 特性和多個返回語句,程式碼可以有多簡潔。

const checkModel = ({model, year} = {}) => {
  if(!model && !year) return 'No car';
  if(!model) return 'No car model';
  if(!year) return 'No car year';

  // 這裡可以任意操作模型或年份
  // 確保它們存在
  // 無需更多檢查

  // doSomething(model);
  // doSomethingElse(year);
  
  return `Car model: ${model}; Manufacturing year: ${year};`;
}

console.log(checkModel()); // 輸出 'No car'
console.log(checkModel({ year: 1988 })); // 輸出 'No car model'
console.log(checkModel({ model: 'ford' })); // 輸出 'No car year'
console.log(checkModel({ model: 'ford', year: 1988 })); // 輸出 'Car model: ford; Manufacturing year: 1988;'

在重構版本中,我們包含了解構和預設引數。預設引數確保我們在傳入 undefined 時有可用於解構的值。注意,如果傳入 null ,函式將會丟擲錯誤。這也是之前那個方法的優點所在,因為那個方法在傳入 null 的時候會輸出 'No car'

物件解構確保函式只取所需。例如,如果我們在給定車輛物件中包含額外屬性,則該屬性在我們的函式中是無法獲取的。

根據偏好,開發者會選擇其中一種方式。實踐中,編寫的程式碼通常介於兩者之間。很多人覺得 if...else 語句更容易理解,並且有助於他們更為輕鬆地遵循程式流程。

6. 使用索引或者對映,而不是 switch 語句

假設我們想要基於給定的國家獲取汽車模型。

const getCarsByState = (state) => {
  switch (state) {
    case 'usa':
      return ['Ford', 'Dodge'];
    case 'france':
      return ['Renault', 'Peugeot'];
    case 'italy':
      return ['Fiat'];
    default:
      return [];
  }
}

console.log(getCarsByState()); // 輸出 []
console.log(getCarsByState('usa')); // 輸出 ['Ford', 'Dodge']
console.log(getCarsByState('italy')); // 輸出 ['Fiat']

上訴程式碼可以重構,完全去除 switch 語句。

const cars = new Map()
  .set('usa', ['Ford', 'Dodge'])
  .set('france', ['Renault', 'Peugeot'])
  .set('italy', ['Fiat']);

const getCarsByState = (state) => {
  return cars.get(state) || [];
}

console.log(getCarsByState()); // 輸出 []
console.log(getCarsByState('usa')); //輸出 ['Ford', 'Dodge']
console.log(getCarsByState('italy')); // 輸出 ['Fiat']

或者,我們還可以為包含可用汽車列表的每個國家建立一個類,並在需要的時候使用。不過這個就是題外話了,本文的主題是關於條件句的。更恰當的修改是使用物件字面量。

const carState = {
  usa: ['Ford', 'Dodge'],
  france: ['Renault', 'Peugeot'],
  italy: ['Fiat']
};

const getCarsByState = (state) => {
  return carState[state] || [];
}

console.log(getCarsByState()); // 輸出 []
console.log(getCarsByState('usa')); // 輸出 ['Ford', 'Dodge']
console.log(getCarsByState('france')); // 輸出 ['Renault', 'Peugeot']

7. 使用自判斷連結和空合併

到了這一小節,我終於可以說“最後”了。在我看來,這兩個功能對於 JavaScript 語言來說是非常有用的。作為一個來自 C# 世界的人,可以說我經常使用它們。

在寫這篇文章的時候,這些還沒有得到完全的支援。因此,對於以這種方式編寫的程式碼,你需要使用 Babel 進行編譯。你可以在自判斷連結這裡以及在空合併這裡查閱。

自判斷連結允許我們在沒有顯式檢查中間節點是否存在的時候處理樹形結構,空合併可以確保節點不存在時會有一個預設值,配合自判斷連結使用會有不錯的效果。

讓我們用一些例子來支撐上面的結論。一開始,我們還是用以前的老方法:

const car = {
  model: 'Fiesta',
  manufacturer: {
    name: 'Ford',
    address: {
      street: 'Some Street Name',
      number: '5555',
      state: 'USA'
    }
  }
}

// 獲取汽車模型
const model = car && car.model || 'default model';
// 獲取廠商地址
const street = car && car.manufacturer && car.manufacturer.address && car.manufacturer.address.street || 'default street';
// 請求一個不存在的屬性
const phoneNumber = car && car.manufacturer && car.manufacturer.address && car.manufacturer.phoneNumber;

console.log(model) // 輸出 'Fiesta'
console.log(street) // 輸出 'Some Street Name'
console.log(phoneNumber) // 輸出 undefined

因此,如果我們想要知道廠商是否來自 USA 並將結果列印,那麼程式碼是這樣的:

const checkCarManufacturerState = () => {
  if(car && car.manufacturer && car.manufacturer.address && car.manufacturer.address.state === 'USA') {
    console.log('Is from USA');
  }
}

checkCarManufacturerState() // 輸出 'Is from USA'

我無需再贅述如果物件結構更加複雜的話,程式碼會多麼混亂了。許多庫,例如 lodash,有自己的函式作為替代方案。不過這不是我們想要的,我們想要的是在原生 js 中也能做同樣的事。我們來看一下新的方法:

    // 獲取汽車模型
    const model = car?.model ?? 'default model';
    // 獲取廠商地址
    const street = car?.manufacturer?.address?.street ?? 'default street';
    
    // 檢查汽車廠商是否來自 USA
    const checkCarManufacturerState = () => {
      if(car?.manufacturer?.address?.state === 'USA') {
        console.log('Is from USA');
      }
    }

這看起來更加漂亮和簡潔,對我來說,非常符合邏輯。如果你想知道為什麼應該使用 ?? 而不是 || ,只需想一想什麼值可以當做 true 或者 false,你將可能有意想不到的輸出。

順便說句題外話。自判斷連結同樣支援 DOM API,這非常酷,意味著你可以這麼做:

const value = document.querySelector('input#user-name')?.value;

結論

好了,這就是全部內容了。如果你喜歡這篇文章的話,可以送一杯咖啡給我,讓我提提神,還可以訂閱文章或者在 twitter 上關注我。

感謝閱讀,下篇文章見。


譯者注:
關於最後一個例子的空合併為什麼使用 ?? 而不是 ||,作者可能解釋得不是很清楚,這裡摘抄一下 tc39:proposal-nullish-coalescing 的例子:

const headerText = response.settings?.headerText || 'Hello, world!'; // '' 會被當作 false,輸出: 'Hello, world!'
const animationDuration = response.settings?.animationDuration || 300; // 0 會被當作 false,輸出: 300
const showSplashScreen = response.settings?.showSplashScreen || true; // False 會被當作 false,輸出: true

照理來說,使用 || 是可以的,但是在上面程式碼中會有點小問題。比如我們想要獲取的 animationDuration 的值為 0,那麼由於 0 被當作 false,導致我們最後得到的是預設值 300,這顯然不是我們想要的結果。而 ?? 就是用來解決這個問題的。
目前 optional-chaining 和 nullish-coalescing 還在 ecma 標準草案的 stage2 階段,不過 babel 針對前者已有相關外掛實現,更多相關文章可以看:
https://segmentfault.com/a/11...
https://zhuanlan.zhihu.com/p/...
https://www.npmjs.com/package...

相關文章