JS 寫邏輯判斷,不要只知道用 if-else 和 switch

前端小蜜蜂發表於2020-05-26

我們在編寫 JS 程式碼時,經常會遇到邏輯判斷複雜的情況。一般情況下,可以用 if/else 或 switch 來實現多個條件判斷,但會出現一個問題:隨著邏輯複雜度的增加,程式碼中的 if/else 和 switch 會越來越臃腫。本文將帶你嘗試寫出更優雅的判斷邏輯。

比如說下面這樣一段程式碼:

const onButtonClick = (status) => {
  if (status == 1) {
    sendLog('processing')
    jumpTo('IndexPage')
  } else if (status == 2) {
    sendLog('fail')
    jumpTo('FailPage')
  } else if (status == 3) {
    sendLog('fail')
    jumpTo('FailPage')
  } else if (status == 4) {
    sendLog('success')
    jumpTo('SuccessPage')
  } else if (status == 5) {
    sendLog('cancel')
    jumpTo('CancelPage')
  } else {
    sendLog('other')
    jumpTo('Index')
  }
}

你可以在程式碼中看到這個按鈕的點選邏輯。根據活動狀態的不同做兩件事,傳送日誌埋點並跳轉到相應的頁面。很容易想到這段程式碼可以用 switch 重寫如下:

const onButtonClick = (status) => {
  switch (status) {
    case 1:
      sendLog('processing')
      jumpTo('IndexPage')
      break
    case 2:
    case 3:
      sendLog('fail')
      jumpTo('FailPage')
      break
    case 4:
      sendLog('success')
      jumpTo('SuccessPage')
      break
    case 5:
      sendLog('cancel')
      jumpTo('CancelPage')
      break
    default:
      sendLog('other')
      jumpTo('Index')
      break
  }
}

好吧,看起來比 if/else 層次結構更清晰一些,細心的讀者可能也發現了一個小竅門:case 2 和 case 3 的邏輯一樣時,可以把前面的邏輯處理程式碼省略,case 2 會自動執行與 case 3 的邏輯。

不過,還有一個更簡單的寫法:

const actions = {
  '1': ['processing', 'IndexPage'],
  '2': ['fail', 'FailPage'],
  '3': ['fail', 'FailPage'],
  '4': ['success', 'SuccessPage'],
  '5': ['cancel', 'CancelPage'],
  default: ['other', 'Index'],
}

const onButtonClick = (status) => {
  let action = actions[status] || actions['default'],
    logName = action[0],
    pageName = action[1]
  sendLog(logName)
  jumpTo(pageName)
}

上面的程式碼看起來確實比較乾淨,這種方法的巧妙之處在於,它把判斷條件作為物件的屬性名,把處理邏輯作為物件的屬性值。在點選按鈕的時候,這種方法特別適用於單項條件判斷的情況,即通過物件屬性查詢的方式進行邏輯判斷。

這個方法很好,但是有沒有其他的方法來編碼呢?有的!

const actions = new Map([
  [1, ['processing', 'IndexPage']],
  [2, ['fail', 'FailPage']],
  [3, ['fail', 'FailPage']],
  [4, ['success', 'SuccessPage']],
  [5, ['cancel', 'CancelPage']],
  ['default', ['other', 'Index']],
])

const onButtonClick = (status) => {
  let action = actions.get(status) || actions.get('default')
  sendLog(action[0])
  jumpTo(action[1])
}

使用 Map 代替 Object 有很多優點,Map 物件和普通物件有的區別是:

  • 一個物件通常有自己的原型,所以一個物件總是有一個“prototype”鍵
  • 物件的鍵只能是一個字串或符號,但 Map 的鍵可以是任何值
  • 你可以通過使用 size 屬性很容易得到 Map 中的鍵值對的數量,而一個物件中的鍵值對數量不能直接獲取

現在我們來升級一下這個問題的難度。點選按鈕時,不僅要判斷狀態,還要判斷使用者的身份。

const onButtonClick = (status, identity) => {
  if (identity == 'guest') {
    if (status == 1) {
      //do sth
    } else if (status == 2) {
      //do sth
    } else if (status == 3) {
      //do sth
    } else if (status == 4) {
      //do sth
    } else if (status == 5) {
      //do sth
    } else {
      //do sth
    }
  } else if (identity == 'master') {
    if (status == 1) {
      //do sth
    } else if (status == 2) {
      //do sth
    } else if (status == 3) {
      //do sth
    } else if (status == 4) {
      //do sth
    } else if (status == 5) {
      //do sth
    } else {
      //do sth
    }
  }
}

從上面的例子中可以看到,當你的邏輯升級到雙重判斷的時候,你的判斷力就會加倍,你的程式碼就會加倍。

如何才能讓程式碼更乾淨利落呢?

這裡有一個解決方案。

const actions = new Map([
  ['guest_1', () => {}],
  ['guest_2', () => {}],
  ['guest_3', () => {}],
  ['guest_4', () => {}],
  ['guest_5', () => {}],
  ['master_1', () => {}],
  ['master_2', () => {}],
  ['master_3', () => {}],
  ['master_4', () => {}],
  ['master_5', () => {}],
  ['default', () => {}],
])

const onButtonClick = (identity, status) => {
  let action = actions.get(`${identity}_${status}`) || actions.get('default')
  action.call(this)
}

上述程式碼的核心邏輯是。將兩個判斷條件拼接成一個字串作為 Map 的鍵,然後在查詢時直接查詢對應字串的值。當然,我們也可以在這裡把 Map 改成 Object。

const actions = {
  guest_1: () => {},
  guest_2: () => {},
  //....
}
const onButtonClick = (identity, status) => {
  let action = actions[`${identity}_${status}`] || actions['default']
  action.call(this)
}

如果讀者覺得把查詢拼成一個字串有點尷尬,還有另一個解決辦法,那就是用一個 Map 物件作為 key。

const actions = new Map([
  [{ identity: 'guest', status: 1 }, () => {}],
  [{ identity: 'guest', status: 2 }, () => {}],
  //...
])
const onButtonClick = (identity, status) => {
  let action = [...actions].filter(([key, value]) => key.identity == identity && key.status == status)
  action.forEach(([key, value]) => value.call(this))
}

這裡你也可以看到 Map 和普通物件的區別,其中 Map 可以用任何型別的資料作為鍵。現在讓我們把它的難度再提高一點。如果對於 guest 身份來說,狀態 1-4 的處理邏輯是一樣的呢?

最壞的情況是這樣的(程式碼大量重複):

const actions = new Map([
  [{ identity: 'guest', status: 1 }, () => {}],
  [{ identity: 'guest', status: 2 }, () => {}],
  [{ identity: 'guest', status: 3 }, () => {}],
  [{ identity: 'guest', status: 4 }, () => {}],
  [{ identity: 'guest', status: 5 }, () => {}],
  //...
])

更好的方法是把處理邏輯函式分離出來:

const actions = () => {
  const functionA = () => {}
  const functionB = () => {}
  return new Map([
    [{ identity: 'guest', status: 1 }, functionA],
    [{ identity: 'guest', status: 2 }, functionA],
    [{ identity: 'guest', status: 3 }, functionA],
    [{ identity: 'guest', status: 4 }, functionA],
    [{ identity: 'guest', status: 5 }, functionB],
    //...
  ])
}

const onButtonClick = (identity, status) => {
  let action = [...actions()].filter(([key, value]) => key.identity == identity && key.status == status)
  action.forEach(([key, value]) => value.call(this))
}

這對於日常需求來說已經足夠了,但是說真的,函式 A 被引用了 4 次,還是有點煩人。

如果事情真的變得很複雜,比如身份有 3 種,狀態有 10 種,你需要定義 30 個處理邏輯,其中很多處理邏輯都是一樣的,這似乎讓人無法接受。

而你可以這樣做:

const actions = () => {
  const functionA = () => {} // 邏輯處理 A
  const functionB = () => {} // 邏輯處理 B
  return new Map([
    [/^guest_[1-4]$/, functionA],
    [/^guest_5$/, functionB],
    //...
  ])
}

const onButtonClick = (identity, status) => {
  let action = [...actions()].filter(([key, value]) => key.test(`${identity}_${status}`))
  action.forEach(([key, value]) => value.call(this))
}

這時使用 Map 而不是 Object 的優勢比較明顯,因為可以用正則式作為鍵。

如果需求變成:所有的對 guest 操作都需要傳送一個日誌埋點,不同狀態的 guest 可能有不同的邏輯處理,那麼我們可以寫成如下:

const actions = () => {
  const functionA = () => {} // 邏輯處理 A
  const functionB = () => {} // 邏輯處理 B
  const functionC = () => {} // 傳送日誌 C
  return new Map([
    [/^guest_[1-4]$/, functionA],
    [/^guest_5$/, functionB],
    [/^guest_.*$/, functionC],
    //...
  ])
}

const onButtonClick = (identity, status) => {
  let action = [...actions()].filter(([key, value]) => key.test(`${identity}_${status}`))
  action.forEach(([key, value]) => value.call(this))
}

這樣一來,公共邏輯和單個邏輯可以同時執行。

總結

本文講到了八種 JS 邏輯判斷的寫法,包括:

  1. if/else
  2. switch
  3. 單一判斷:儲存在 Object 中
  4. 單一判斷:儲存在 Map 物件中
  5. 多重判斷:將條件串聯成一個字串,儲存在 Object 中
  6. 多重判斷:將條件連成一個字串,儲存在 Map 物件中
  7. 多重判斷:把條件作為物件儲存在 Map 中
  8. 多重判斷:把條件寫成正則式儲存在 Map 中

今天就分享到這裡,願你今後的編碼生活不僅僅只有 if/else 或 switch。

相關文章