JS條件判斷小技巧


經常code review,我發現很容易寫出一堆冗長的代碼。今天就列幾個比較常見的“解決之道”,看看如何減少JS里的條件判斷。

提前返回,少用if...else

但是過多的嵌套,還是挺令人抓狂的。這里有一個很典型的條件嵌套:

        function func() {
            var result;
            if (conditionA) {
                if (condintionB) {
                    result = 'Success';
                } else {
                    result = 'Error1';
                }
            } else {
                result = 'Error2';
            }
            return result;
        }

這種嵌套的特點就是else里的代碼塊很小,但是由於不得不做的分支語句導致多層嵌套。動動腦筋,怎樣精簡一下呢?在if里做非判斷——條件反轉,並通過衛語句提前return else分支。

        function func() {
            if (!conditionA) {
                return 'Error2';
            }

            if (!condintionB) {
                return 'Error1';
            }
            return 'Success';
        }

forEach優化

遍歷的時候也經常產生大量的嵌套,如下代碼所示,我們先對數組元素做一次合法性校驗,通過后再做一次新的操作,最后把操作結果追加到新數組里。

        const func = (arr) => {
            const res = [];
            arr.forEach((e) => {
                if (e !== 'Onion') {
                    res.push(`${e} Run!`);
                }
            })
            return res;
        }

仔細觀察這就是一個filter加map的過程。我們不妨試試函數式編程:

        const func = (arr) => {
            return arr.filer((e) => e !== 'Onion')
                .map((e) => `${e} Run!`);
        }

多條件,用Array.includes

再舉個例子,某個頁面需要檢驗輸入type是否合法。我收到過一個MR曾經是這么寫的。  

        const init(type) {
            if (type === 'Seminar' || type === 'Interview') {
                console.log('valid');
            }
            //...
            console.error('invalide');
        }

如果合法的類型只有兩種,代碼其實也沒啥問題。只是一般的業務很容易有后續的延展。今后將合法類型增加到10種的話,上述代碼里將是一大長串的if判斷。這種代碼可讀性極差,我們不如轉換一下思想,把非法類型儲到數組里,用Array.includes來幫你完成冗長的判斷。之后每增加一種新的類型,只需在數組后追加新字符串就行了。  

        const init(type) {
            const invalidArray = ['Seminar', 'Interview'];
            if (invalidArray.includes(type)) {
                console.log('valid');
            }    
            //...
            console.error('invalide');
        }

使用object索引

類似的情況也出現在三元表達式里:  

        const dateFormat = (dateTime) => {
            const format = this.$i18n.locale === 'en' ? 'mmm d, yyyy' : 'yyyy年m月d日';
            return DateFormat(dateTime, format);
        }

我們現階段多語言只有en和zh,上述代碼自然不是問題,但是也難保哪天會支持日語——ja。這時候再寫成下面這類代碼就很搞笑了

        const format = this.$i18n.locale === 'en' ? 'mmm d, yyyy' : (
            this.$i18n.locale === 'zh' ? 'yyyy年m月d日' : 'yyyy/m/d'
        );

比較合適的寫法就是使用object鍵索引,這樣當語言業務擴展后,只需要在localeFormats后追加新格式就行了。

        const localeFormats = {
            en: 'mmm d, yyyy',
            zh: 'yyyy年m月d日',
            ja: 'yyyy/m/d',
        };
        const format = localeFormats[this.$i18n.locale];

盡量少用swith

長Switch也及其難看。

        export function(type) {
            switch (type) {
                case 'Onion':
                    return func1();
                case 'Garlic':
                    return func2();
                case 'Ginger':
                    return func3();
                default:
                    return () => {
                        console.error('ERROR')
                    };
            }
        }

我記得OOP設計模式里提到過:盡量使用多態和繼承代替Switch結構。JS里倒不必非得往這方面想,用Object或是Map索引來代替Switch也是極好滴!

        const arr = [['Onion', func1], ['Garlic', func2], ['Ginger', func3],];
        const def = () => { console.error('ERROR') };
        const vegetable = new Map(arr);
        export function(type) {
            return (vegetable.get(type) || def).call(null);
        }

Optional Chaining

Optional Chaining(以下簡稱OC)是我極力推薦的一個語法糖。我寫過一期《Javascript Optional Chaining》具體介紹它的用法,有興趣的小伙伴可以看一看,這里稍微點一下。比如我們想獲取地址里的街道信息,但是並不確定地址本身是否存在,因此只能在獲取街道前,事先判斷一下地址合法性,一般我們會這么寫:  

        if (address) {
            var street = address.street;
        }

但假如再多一層呢,從basicInfo.address.street這樣找下來呢?

        if (basicInfo) {
            var address = basicInfo.address;
            if (address) {
                var street = address.street;
            }
        }

上面的代碼我已經覺得很丑陋了,再多個幾層真是沒法看了。不過不用擔心,有了OC一切迎刃而解。(雖然OC還在ECMAScript stage2,但是大家可以用babel嘗鮮;babel會自動把如下源碼轉義成上面的形式)

var street = basicInfo?.address?.street;

OOP 多態

某些業務邏輯本身就十分復雜,嵌套的條件語句在邏輯層面就不可能有所優化了;碰到這類場景,我們又該如何作為呢?  

        function greeting(role, access) {
            if ('owner' === role) {
                if ('public' === access) {
                    //...
                }
                if ('private' === access) { 
                    //...
                }    
                //...
            } else if ('admin' === role) {
                if ('public' === access) {
                    //...
                }
                if ('private' === access) {       
                    //...
                }    
                //...
            } else if ('hr' === role) {   
                //...
            }
        }

看一下代碼,第一層的if-else判定的是各種角色(role)類別,第二層判定的是角色訪問權限設置(access)。這類代碼其實並沒有特別優雅的處理手段,只能回到《clean code》里最本源的解決手段——把函數寫小。本質問題還是函數體過大,而所謂的把大函數拆成多個小函數,事實上就是以抽象換取可讀性。

最常規的手段就是 OOP 多態了。上述代碼塊,第一層的 role 抽象為 User 實例,嵌套層內的各種 access 進一步抽象為 User 的實例方法。  

        class User {
            public() {
                throw new Error('Denied!');
            }
            private() {
                throw new Error('Denied!');
            }
        }

Javascript 並沒有 interface 這類語法,好在有 class 了,我仿造 interface 寫了一個基類如上。接着就是將各種角色抽象為新的子類型:  

        class Owner extends User {
            public() {
                console.log('Owner in public');
            }
            private() {
                console.log('Owner inside');
            }
        }
        class Admin extends User {
            public() {
                console.log('Admin in public');
            }
            private() {
                console.log('Admin inside');
            }
        }
        //...

OOP 推薦使用工廠方法初始化實例,我順手也寫個工廠,這樣便可以利用工廠方法消除掉了第一層if-else

        class UserFactory {
            static create(role) {
                if ('owner' === role)
                    return new Owner();
                else if ('admin' === role)
                    return new Admin();   
                //...
            }
        }

調用的時候我們先通過 role 創建抽象實例,再根據 access 調用具體方法:  

        function greeting(role, access) {
            const user = UserFactory.create(role);
            user[access]();
        }

上面一長串的if-else,一下子被壓縮到了兩行。這就實現了以抽象(很多可描述的類)換取了可讀性(較少的判斷嵌套)  

調用鏈

OOP 效果確實很明顯,不過上述代碼還是過於特例,假如access並不是字符串(如1,2,3),像user[1]這種就很難映射到具體方法了;所以我們往往還要寫更細碎的 access 抽象,也便意味着更多的抽象子類,以及新的工廠方法。很多時候,我們也不並需要抽象得盡善盡美。這個場景里寫個調用鏈,也是勉強可用的:

        const rules = [
            {
                match(role, access) {
                    return 'owner' === role;
                },
                action(role, access) {
                    if (1 === access)
                        console.log('Owner in public');
                    else if (2 === access)
                        console.log('Owner in private');
                }
            },
            {
                match(role, access) {
                    return 'admin' === role;
                },
                action(role, access) {
                    //...    
                }
            }  
            //...
        ];

上面 rules 數組里,每一個元素(rule)里的match被設計用來判定用戶權限:遍歷數組,若是match為 true,則運行正下方的action——access 相關業務;反之,繼續match下一個 rule:

        function greeting(role, access) {
            rules.find(e => e.match(role))
                .action(role, access);
        }

最后 greeting 被重構為上述代碼。當然,效果沒有多態好,只消掉了一層if-else,第二層判定還是留在了 action 里。  

AOP

AOP,沒看錯,Javascript 也是有 AOP 的,只是它的實現要修改 Function 的原型鏈,不是很推薦;但是Function.prototype.before,Function.prototype.after還是挺常見的,開發組里能協商好,還是可以嘗試一下的:

        Function.prototype.after = function (next) {
            let fn = this;
            return function $after(...args) {
                let code = fn.apply(this, args);
                next.apply(this, args);
                return code;
            }
        }

傳統的 aop after 如上所示。不難看出,用到了高階函數:具體執行時,先運行函數本體,再運行 after 傳進來的 next 方法。為了讓 after 應用到我們的話題中,我稍微改一下函數實現:  

        const nextSmb = Symbol('next');
        Function.prototype.after = function (next) {
            let fn = this;
            return function $after(...args) {
                let code = fn.apply(this, args);
                if (nextSmb === code)
                    return next.apply(this, args);
                return code;
            }
        }

這個 after 實現變成了先運行函數本體,若返回是nextSmb則繼續執行后續的 next 方法,反之則停止。有什么用呢?我們看看如何使用:  

        function owner(role, access) {
            function public(access) {
                return 1 === access ? console.log('owner in public') : nextSmb;
            }
            function private(access) {
                return 2 === access ? console.log('owner in private') : nextSmb;
            }
            const ownerChain = public.after(private);
            return 'owner' === role ? ownerChain(access) : nextSmb;
        }

代碼還是有點難度的,先看一部分——owner 的定義。這個函數被設計處理role === 'owner'時的邏輯,內部的public和private方法是處理access為 1 和 2 時的邏輯。我們把public和private方法串聯成ownerChain(終於用到after方法了),它的作用就是把之前的if-else邏輯抽象成一個上節講到的函數調用鏈,在遍歷調用鏈時檢查 access 條件:若符合條件,則執行本節點代碼,並結束調用鏈;反之,繼續往調用鏈的后續節點傳送。

我把重構后的 greeting 也列一下——單個role的access可以用after串聯;不同role之間也可以進一步利用after串起來。

        function admin(role, access) {
            // familiar with owner
            function public(access) {
                // 
            }
            function private(access) {
                // 
            }
            const ownerChain = public.after(private);
            return 'admin' === role ? ownerChain(access) : nextSmb;
        }
        let greeting = owner.after(admin);
        greeting('owner', 1);

嗯,這樣,我們最原始的greeting方法就被徹底重構了。可以預見,如果調用鏈很長greeting會是這樣:  

let greeting = owner.after(admin).after(hr).after(staff)...

當然這個方法缺點也很明確,比起之前冗長的代碼,可讀性增強了,但是理解成本有點高,若團隊內沒有事先約定,這個維護起來還是挺難的。  

作者:anOnion
鏈接:https://www.jianshu.com/p/6024b1796af5  

  

  

  

  

  

  

  

  

  

  

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM