在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提案中了。我们再等等就可以用上了。

总结

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

你可能感兴趣的:(在JavaScript中优雅的进行条件判断)