在JavaScript中优雅的进行条件判断

本文是一篇翻译文章。

原文链接: https://dev.to/hellomeghna/tips-to-write-better-conditionals-in-javascript-2189

什么是条件语句

不管是什么编程语言,代码中都少不了做判断,以及依据一个输入的条件决定要执行不同的操作。

例如,在一款游戏中,如果玩家的生命数小于0,游戏就宣告结束。在一款天气预报app中,在早上显示太阳的图片,在夜间显示星星和月亮的图片。在本文中我们就将介绍如何在JavaScript中进行类似的条件处理。

当你写JavaScript时,有时候会写出一大堆包含许多条件判断的代码。这些条件语句,在开始的时候可能还比较好理解,但是过一阵子之后就变得一团糟了。其实有比if/else更棒的方式去实现条件判断。

这里就有一些关于如何写出干净优雅的条件判断的建议。

目录

  1. Array.includes
  2. 尽早退出和返回
  3. 用Object遍历 或者 Map 取代 Switch 表达式
  4. 使用默认参数 和 解构赋值
  5. 使用Array.every 和 Array.some去实现所有 和 部分条件判断
  6. Use Optional Chaining and Nullish Coalescing

1.Array.includes

如果有多个条件可以使用Array.includes

例如:

function printAnimals(animal) {
    if (animal === 'dog' || animal === 'cat') {
        console.log(`I have a ${animal}`);
    }
}

console.log(printAnimals('dog')); // I have a dog

上面的代码似乎看起来还行,那是因为我们只需要检测两种小动物。然而我们并不确定用户会输入什么。如果动物的类型变多了呢?如果我们继续通过扩展|| 条件判断来满足需求,我们的代码会变得越来越难维护,并且看起来乱乱的。

解决方案:

我们可以用 Array.includes来重构一下上面的代码 :

function printAnimals(animal) {
    const animals = ['dog', 'cat', 'hamster', 'turtle'];

    if (animals.includes(animal)) {
        console.log(`I have a ${animal}`);
    }
}

console.log(printAnimals('hamster')); // I have a hamster

这里我们创建了一个数组存放动物,这样判断条件就可以和剩余的代码隔离开了。现在,如果我们想继续扩充条件,我们只需要往数组里添加新的元素就可以了。(清晰多了呢)

2.尽早退出和返回

这是一个非常酷的小技巧,可以使你的代码看起来简洁。我记得我从工作的第一天起,我就被教导,在条件判断时要early exit(尽早退出)。

让我们为上一个示例多添加些条件。如果animal 不再是一个简单的string了,而是一个有特定属性的object

所以现在需求变成了下面这样:

  • 如果没有animal,抛出一个错误
  • 打印出animal的类型
  • 打印出animal的名字
  • 打印出animal的类型
const printAnimalDetails = animal => {
    let result; // declare a variable to store the final value

    // condition 1: check if animal has a value
    if (animal) {

        // condition 2: check if animal has a type property
        if (animal.type) {

            // condition 3: check if animal has a name property
            if (animal.name) {

                // condition 4: check if animal has a gender property
                if (animal.gender) {
                    result = `${animal.name} is a ${animal.gender} ${animal.type};`;
                } else {
                    result = "No animal gender";
                }
            } else {
                result = "No animal name";
            }
        } else {
            result = "No animal type";
        }
    } else {
        result = "No animal";
    }

    return result;
};

console.log(printAnimalDetails()); // 'No animal'

console.log(printAnimalDetails({type: "dog", gender: "female"})); // 'No animal name'

console.log(printAnimalDetails({type: "dog", name: "Lucy"})); // 'No animal gender'

console.log(
    printAnimalDetails({type: "dog", name: "Lucy", gender: "female"})
); // 'Lucy is a female dog'

对于上面的代码,你怎么看呢?

上面的代码没什么bug,但是看起来太长了,而且很难维护呢。一个新人可能得花个一上午来找哪些括号是一对的呢(手动滑稽)。如果逻辑再复杂点呢,if/else就更多了。我们可以用?:$$运算符等来重构上面的代码。但是,我就不(哈哈哈……)。我使用了多次return来重构了上面的代码。

const printAnimalDetails = ({type, name, gender } = {}) => {
    if(!type) return 'No animal type';
    if(!name) return 'No animal name';
    if(!gender) return 'No animal gender';

// Now in this line of code, we're sure that we have an animal with all //the three properties here.

    return `${name} is a ${gender} ${type}`;
}

console.log(printAnimalDetails()); // 'No animal type'

console.log(printAnimalDetails({ type: dog })); // 'No animal name'

console.log(printAnimalDetails({ type: dog, gender: female })); // 'No animal name'

console.log(printAnimalDetails({ type: dog, name: 'Lucy', gender: 'female' })); // 'Lucy is a female dog'

在重构版中,也用到了对象的解构赋值和函数参数的默认值。默认值的作用是,如果我们没有传参(undifined),也能保证不会报错。

另一个例子:

function printVegetablesWithQuantity(vegetable, quantity) {
    const vegetables = ['potato', 'cabbage', 'cauliflower', 'asparagus'];

    // condition 1: vegetable should be present
    if (vegetable) {
        // condition 2: must be one of the item from the list
        if (vegetables.includes(vegetable)) {
            console.log(`I like ${vegetable}`);

            // condition 3: must be large quantity
            if (quantity >= 10) {
                console.log('I have bought a large quantity');
            }
        }
    } else {
        throw new Error('No vegetable from the list!');
    }
}

printVegetablesWithQuantity(null); //  No vegetable from the list!
printVegetablesWithQuantity('cabbage'); // I like cabbage
printVegetablesWithQuantity('cabbage', 20);
// 'I like cabbage`
// 'I have bought a large quantity'

现在,上面的例子中包含:

  • 1对if/else用来过滤不可用的条件
  • 3级嵌套if

接下来我要介绍我们的套路了—— 当遇到不可用的条件时尽早退出函数

function printVegetablesWithQuantity(vegetable, quantity) {

    const vegetables = ['potato', 'cabbage', 'cauliflower', 'asparagus'];

    // condition 1: throw error early
    if (!vegetable) throw new Error('No vegetable from the list!');

    // condition 2: must be in the list
    if (vegetables.includes(vegetable)) {
        console.log(`I like ${vegetable}`);

        // condition 3: must be a large quantity
        if (quantity >= 10) {
            console.log('I have bought a large quantity');
        }
    }
}

这样重构后,我们就少了一层if嵌套,当你的条件判断比较长时,这种代码风格尤为好用。

我们能进一步减少if嵌套,通过对条件进行取反,然后return。下面就是具体实现:

function printVegetablesWithQuantity(vegetable, quantity) {

    const vegetables = ['potato', 'cabbage', 'cauliflower', 'asparagus'];

    if (!vegetable) throw new Error('No vegetable from the list!');
    // condition 1: throw error early

    if (!vegetables.includes(vegetable)) return;
    // condition 2: return from the function is the vegetable is not in 
    //  the list 


    console.log(`I like ${vegetable}`);

    // condition 3: must be a large quantity
    if (quantity >= 10) {
        console.log('I have bought a large quantity');
    }
}

通过对第二个条件取反,代码里再也看不到if的嵌套了。这种技巧适用于当我们有好多条件判断,并且当满足某一个时,不再进行剩余的逻辑处理。

因此,我们的目标是消灭嵌套,及早return。但是return大法好,也不能"贪杯"啊~

3. 用Object遍历 或者 Map 取代 Switch 表达式

让我们看下这个例子,我们想基于颜色打印出水果:

function printFruits(color) {
    // use switch case to find fruits by color
    switch (color) {
        case 'red':
            return ['apple', 'strawberry'];
        case 'yellow':
            return ['banana', 'pineapple'];
        case 'purple':
            return ['grape', 'plum'];
        default:
            return [];
    }
}

printFruits(null); // []
printFruits('yellow'); // ['banana', 'pineapple']

上面的代码没什么错误,就是看起来有点长。我们可以用Object来实现同样的效果:

// use object literal to find fruits by color
const fruitColor = {
    red: ['apple', 'strawberry'],
    yellow: ['banana', 'pineapple'],
    purple: ['grape', 'plum']
};

function printFruits(color) {
    return fruitColor[color] || [];
}

当然也可以用Map

// use Map to find fruits by color
const fruitColor = new Map()
    .set('red', ['apple', 'strawberry'])
    .set('yellow', ['banana', 'pineapple'])
    .set('purple', ['grape', 'plum']);

function printFruits(color) {
    return fruitColor.get(color) || [];
}

Map是ES2015 (Es6)的语法,大家注意兼容性呀!

也能用Array.filter来实现:

const fruits = [
    { name: 'apple', color: 'red' },
    { name: 'strawberry', color: 'red' },
    { name: 'banana', color: 'yellow' },
    { name: 'pineapple', color: 'yellow' },
    { name: 'grape', color: 'purple' },
    { name: 'plum', color: 'purple' }
];

function printFruits(color) {
    return fruits.filter(fruit => fruit.color === color);
}

4.使用默认参数 和 解构赋值

我们写JavaScript时,经常需要去检查null/undefined,并对参数赋默认值,否则就会报错。

function printVegetablesWithQuantity(vegetable, quantity = 1) {
// if quantity has no value, assign 1

    if (!vegetable) return;
    console.log(`We have ${quantity} ${vegetable}!`);
}

//results
printVegetablesWithQuantity('cabbage'); // We have 1 cabbage!
printVegetablesWithQuantity('potato', 2); // We have 2 potato!

如果vegetable是一个对象呢?我们能给它默认赋值么?

function printVegetableName(vegetable) {
    if (vegetable && vegetable.name) {
        console.log (vegetable.name);
    } else {
        console.log('unknown');
    }
}

printVegetableName(undefined); // unknown
printVegetableName({}); // unknown
printVegetableName({ name: 'cabbage', quantity: 2 }); // cabbage

上面的例子中,如果vegetable有可用的值,我们就打印出它的name,否则打印unknow

我们可以用默认值 和 解构赋值代替if (vegetable && vegetable.name) {}

// destructing - get name property only
// assign default empty object {}

function printVegetableName({name} = {}) {
    console.log (name || 'unknown');
}


printVegetableName(undefined); // unknown
printVegetableName({ }); // unknown
printVegetableName({ name: 'cabbage', quantity: 2 }); // cabbage

因为我们只需要name属性,我们可以{name}将它解构出来,然后我们就可以使用name变量了,这样就不需要使用vegetable.name了。

我们也给函数的参数设置了一个默认值{},否则当我们执行printVegeTable(undefined)的时就会报错Cannot destructure property name of undefined or null,因为undefined不是对象,是不能解构的。

5.使用Array.every 和 Array.some去实现所有 和 部分条件判断

我么可以使用数组的这些方法来减少代码行数。下面的代码中我们想判断是否所有的水果都是红色。

const fruits = [
    { name: 'apple', color: 'red' },
    { name: 'banana', color: 'yellow' },
    { name: 'grape', color: 'purple' }
];

function test() {
    let isAllRed = true;

    // condition: all fruits must be red
    for (let f of fruits) {
        if (!isAllRed) break;
        isAllRed = (f.color == 'red');
    }

    console.log(isAllRed); // false
}

代码太长了。我们可以换Array.every试试:

const fruits = [
    { name: 'apple', color: 'red' },
    { name: 'banana', color: 'yellow' },
    { name: 'grape', color: 'purple' }
];

function test() {
    // condition: short way, all fruits must be red
    const isAllRed = fruits.every(f => f.color == 'red');

    console.log(isAllRed); // false
}

同理,如果我们想判断部分水果是红色的,我们可以用Array.some来实现。

const fruits = [
    { name: 'apple', color: 'red' },
    { name: 'banana', color: 'yellow' },
    { name: 'grape', color: 'purple' }
];

function test() {
    // condition: if any fruit is red
    const isAnyRed = fruits.some(f => f.color == 'red');

    console.log(isAnyRed); // true
}

6. Use Optional Chaining and Nullish Coalescing

这两个功能在JavaScript中是非常有用的。但是目前支持力度还不是很好,所以需要使用babel进行编译。

Optional chaining使我们可以跳过中间层级去检查一个树状解构是否包含某个属性。nullish coalescing可以和

Optional chaining配合使用,来为变量赋默认值。

下面是个例子:

const car = {
    model: 'Fiesta',
    manufacturer: {
        name: 'Ford',
        address: {
            street: 'Some Street Name',
            number: '5555',
            state: 'USA'
        }
    }
}

// to get the car model
const model = car && car.model || 'default model';

// to get the manufacturer street
const street = car && car.manufacturer && car.manufacturer.address &&
    car.manufacturer.address.street || 'default street';

// request an un-existing property
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

所以如果我们想一辆汽车的制造商是否是美国,我们必须这么写代码:

const isManufacturerFromUSA = () => {
    if(car && car.manufacturer && car.manufacturer.address &&
        car.manufacturer.address.state === 'USA') {
        console.log('true');
    }
}


checkCarManufacturerState() // 'true'

你能看到,这么写代码是多么的凌乱。早已经有一些第三方库,像lodash或者idx有自己的函数,去简化这个操作。例如lodash_.get。然而,如果JavaScript能原生支持这种操作就更好了。

下面就是一个例子:

// to get the car model
const model = car?.model ?? 'default model';

// to get the manufacturer street
const street = car?.manufacturer?.address?.street ?? 'default street';

// to check if the car manufacturer is from the USA
const isManufacturerFromUSA = () => {
    if(car?.manufacturer?.address?.state === 'USA') {
        console.log('true');
    }
}

这个代码看起来漂亮多了,而且易于维护。这个特性已经在 TC39 stage 3提案中了。我们再等等就可以用上了。

总结

让我们试着使用这些建议来写一些干净易于维护的代码吧,因为那些冗长的条件判断,再过几个月之后连你自己都看不懂了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269