JavaScript複雜判斷的更優雅寫法

技術小能手發表於2018-11-13

前提

我們編寫js程式碼時經常遇到複雜邏輯判斷的情況,通常大家可以用if/else或者switch來實現多個條件判斷,但這樣會有個問題,隨著邏輯複雜度的增加,程式碼中的if/else/switch會變得越來越臃腫,越來越看不懂,那麼如何更優雅的寫判斷邏輯,本文帶你試一下。

舉個例子

先看一段程式碼


/**

 * 按鈕點選事件

 * @param {number} status 活動狀態:1 開團進行中 2 開團失敗 3 商品售罄 4 開團成功 5 系統取消

 */

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出場:


/**

 * 按鈕點選事件

 * @param {number} status 活動狀態:1 開團進行中 2 開團失敗 3 商品售罄 4 開團成功 5 系統取消

 */

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邏輯一樣的時候,可以省去執行語句和break,則case 2的情況自動執行case 3的邏輯。

這時有同學會說,還有更簡單的寫法:


const actions = {

  `1`: [`processing`, `IndexPage`],

  `2`: [`fail`, `FailPage`],

  `3`: [`fail`, `FailPage`],

  `4`: [`success`, `SuccessPage`],

  `5`: [`cancel`, `CancelPage`],

  `default`: [`other`, `Index`],

}

/**

 * 按鈕點選事件

 * @param {number} status 活動狀態:1開團進行中 2開團失敗 3 商品售罄 4 開團成功 5 系統取消

 */

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`]]

])

/**

 * 按鈕點選事件

 * @param {number} status 活動狀態:1 開團進行中 2 開團失敗 3 商品售罄 4 開團成功 5 系統取消

 */

const onButtonClick = (status) => {

  let action = actions.get(status) || actions.get(`default`)

  sendLog(action[0])

  jumpTo(action[1])

}

這樣寫用到了es6裡的Map物件,是不是更爽了?Map物件和Object物件有什麼區別呢?

  1. 一個物件通常都有自己的原型,所以一個物件總有一個”prototype”鍵。

  2. 一個物件的鍵只能是字串或者Symbols,但一個Map的鍵可以是任意值。

  3. 你可以通過size屬性很容易地得到一個Map的鍵值對個數,而物件的鍵值對個數只能手動確認。

我們需要把問題升級一下,以前按鈕點選時候只需要判斷status,現在還需要判斷使用者的身份:


/**

 * 按鈕點選事件

 * @param {number} status 活動狀態:1開團進行中 2開團失敗 3 開團成功 4 商品售罄 5 有庫存未開團

 * @param {string} identity 身份標識:guest客態 master主態

 */

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

    }

  }

}

原諒我不寫每個判斷裡的具體邏輯了,因為程式碼太冗長了。

原諒我又用了if/else,因為我看到很多人依然在用if/else寫這種大段的邏輯判斷。

從上面的例子我們可以看到,當你的邏輯升級為二元判斷時,你的判斷量會加倍,你的程式碼量也會加倍,這時怎麼寫更清爽呢?


const actions = new Map([

  [`guest_1`, () => { /*do sth*/ }],

  [`guest_2`, () => { /*do sth*/ }],

  [`guest_3`, () => { /*do sth*/ }],

  [`guest_4`, () => { /*do sth*/ }],

  [`guest_5`, () => { /*do sth*/ }],

  [`master_1`, () => { /*do sth*/ }],

  [`master_2`, () => { /*do sth*/ }],

  [`master_3`, () => { /*do sth*/ }],

  [`master_4`, () => { /*do sth*/ }],

  [`master_5`, () => { /*do sth*/ }],

  [`default`, () => { /*do sth*/ }],

])


/**

 * 按鈕點選事件

 * @param {string} identity 身份標識:guest客態 master主態

 * @param {number} status 活動狀態:1 開團進行中 2 開團失敗 3 開團成功 4 商品售罄 5 有庫存未開團

 */

const onButtonClick = (identity, status) => {

  let action = actions.get(`${identity}_${status}`) || actions.get(`default`)

  action.call(this)

}

上述程式碼核心邏輯是:把兩個條件拼接成字串,並通過以條件拼接字串作為鍵,以處理函式作為值的Map物件進行查詢並執行,這種寫法在多元條件判斷時候尤其好用。

當然上述程式碼如果用Object物件來實現也是類似的:


const actions = {

  `guest_1`: () => { /*do sth*/ },

  `guest_2`: () => { /*do sth*/ },

  //….

}


const onButtonClick = (identity, status) => {

  let action = actions[`${identity}_${status}`] || actions[`default`]

  action.call(this)

}

如果有些同學覺得把查詢條件拼成字串有點彆扭,那還有一種方案,就是用Map物件,以Object物件作為key:


const actions = new Map([

  [{

    identity: `guest`,

    status: 1

  }, () => { /*do sth*/ }],

  [{

    identity: `guest`,

    status: 2

  }, () => { /*do sth*/ }],

  //…

])


const onButtonClick = (identity, status) => {

  let action = [actions].filter(([key, value]) => (key.identity == identity && key.status == status))

  action.forEach(([key, value]) => value.call(this))

}

是不是又高階了一點點?

這裡也看出來Map與Object的區別,Map可以用任何型別的資料作為key。

我們現在再將難度升級一點點,假如guest情況下,status1-4的處理邏輯都一樣怎麼辦,最差的情況是這樣:


const actions = 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 actions = () => {

  const functionA = () => { /*do sth*/ }

  const functionB = () => { /*do sth*/ }

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

}

這樣寫已經能滿足日常需求了,但認真一點講,上面重寫了4次functionA還是有點不爽,假如判斷條件變得特別複雜,比如identity有3種狀態,status有10種狀態,那你需要定義30條處理邏輯,而往往這些邏輯裡面很多都是相同的,這似乎也是筆者不想接受的,那可以這樣實現:


const actions = () => {

  const functionA = () => { /*do sth*/ }

  const functionB = () => { /*do sth*/ }

  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的優勢更加凸顯,可以用正則型別作為key了,這樣就有了無限可能,假如需求變成,凡是guest情況都要傳送一個日誌埋點,不同status情況也需要單獨的邏輯處理,那我們可以這樣寫:


const actions = () => {

  const functionA = () => { /*do sth*/ }

  const functionB = () => { /*do sth*/ }

  const functionC = () => { /*send log*/ }

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

}

也就是說利用陣列迴圈的特性,符合正則條件的邏輯都會被執行,那就可以同時執行公共邏輯和單獨邏輯,因為正則的存在,你可以開啟想象力解鎖更多的玩法,本文就不贅述了。

總結

本文已經教你了8種邏輯判斷寫法,包括:

  1. if/else

  2. switch

  3. 一元判斷時:存到Object裡

  4. 一元判斷時:存到Map裡

  5. 多元判斷時:將condition拼接成字串存到Object裡

  6. 多元判斷時:將condition拼接成字串存到Map裡

  7. 多元判斷時:將condition存為Object存到Map裡

  8. 多元判斷時:將condition寫作正則存到Map裡

至此,本文也將告一段落,願你未來的人生裡,不只是有if/else/switch。

原文釋出時間為:2018-11-13
本文作者:大轉轉FE/Think
本文來自雲棲社群合作伙伴“程式設計微刊”,瞭解相關資訊可以關注“程式設計微刊”。


相關文章