JavaScript 複雜判斷的更優雅寫法

  • 2019 年 11 月 25 日
  • 筆記

作者 Think. https://juejin.im/post/5bdfef86e51d453bf8051bf8

前提

我們編寫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')        }elseif(status == 2){          sendLog('fail')          jumpTo('FailPage')        }elseif(status == 3){          sendLog('fail')          jumpTo('FailPage')        }elseif(status == 4){          sendLog('success')          jumpTo('SuccessPage')        }elseif(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){          case1:            sendLog('processing')            jumpTo('IndexPage')            breakcase2:          case3:            sendLog('fail')            jumpTo('FailPage')            breakcase4:            sendLog('success')            jumpTo('SuccessPage')            breakcase5:            sendLog('cancel')            jumpTo('CancelPage')            breakdefault:            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 = newMap([        [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          }elseif(status == 2){            //do sth          }elseif(status == 3){            //do sth          }elseif(status == 4){            //do sth          }elseif(status == 5){            //do sth          }else {            //do sth          }        }elseif(identity == 'master') {          if(status == 1){            //do sth          }elseif(status == 2){            //do sth          }elseif(status == 3){            //do sth          }elseif(status == 4){            //do sth          }elseif(status == 5){            //do sth          }else {            //do sth          }        }      }

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

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

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

    const actions = newMap([        ['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 = newMap([        [{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 = newMap([        [{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*/}        returnnewMap([          [{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*/}        returnnewMap([          [/^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*/}        returnnewMap([          [/^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))      }

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