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

介紹

若是你像我同樣樂於見到整潔的代碼,那麼你會盡量地減小代碼中的條件語句。一般狀況下,面向對象編程讓咱們得以免條件式,並代之以繼承和多態。我認爲咱們應當儘量地遵循這些原則。javascript

正如我在另外一篇文章 JavaScript 整潔代碼的最佳實踐裏提到的,你寫的代碼不僅僅是給機器看的,仍是給「將來的本身」以及「其餘人」看的。 java

從另外一方面來講,因爲各式各樣的緣由,可能咱們的代碼最終仍是會有條件式。也許是修復 bug 的時間很緊,也許是不使用條件語句會對咱們的代碼庫形成大的改動,等等。本文將會解決這些問題,同時幫助你組織所用的條件語句。git

技巧

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

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

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

很差的:編程

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

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

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

好的:segmentfault

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

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

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

如今,理清了上面的事情後,咱們就能夠開始了。數組

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

假設咱們想要在函數中檢查汽車模型是 renault 仍是 peugeot。那麼代碼多是這樣的:babel

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...

相關文章
相關標籤/搜索