Skip to content

JS 代码简洁之道 - 条件语句 #15

@jtwang7

Description

@jtwang7

JS 代码简洁之道 - 条件语句

参考文章:

前言

在用 JavaScript 工作时,我们经常和条件语句打交道,这里有5条让你写出更好/干净的条件语句的建议。

  1. 多重判断时使用 Array.includes
  2. 更少的嵌套,尽早 return
  3. 使用默认参数和解构
  4. 倾向于遍历对象而不是 Switch 语句
  5. 对 "所有/部分" 判断使用 Array.every & Array.some

Array.includes

function test(fruit) {
  // 如果 fruit 是 apple 或 strawberry,则打印 red
  if (fruit == 'apple' || fruit == 'strawberry') {
    console.log('red');
  }
}

缺点:扩展性不强。假如需要添加判断更多 red fruit,只能用更多的 || 来拓展条件语句。

function test(fruit) {
  const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];
  // Array.prototype.includes()
  if (redFruits.includes(fruit)) {
    console.log('red');
  }
}

优点:利用了数组“动态、灵活”的特性,将判断条件从 if 中提取出来,方便调整。

Array.prototype.includes()

作用:判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。
语法:arr.includes(valueToFind[, fromIndex])
参数:

  • valueToFind
    需要查找的元素值。

Note: 使用 includes()比较字符串和字符时是区分大小写。

  • fromIndex (可选)
    从 fromIndex 索引处开始查找 valueToFind。如果为负值,则按升序从 array.length + fromIndex 的索引开始搜 (即使从末尾开始往前跳 fromIndex 的绝对值个索引,然后往后搜寻)。默认为 0。

如果 fromIndex 大于等于数组的长度,则会返回 false,且该数组不会被搜索。

返回值:
返回一个布尔值 Boolean ,如果在数组中找到了(如果传入了 fromIndex ,表示在 fromIndex 指定的索引范围中找到了)则返回 true 。

includes() 方法有意设计为通用方法。它不要求this值是数组对象,所以它可以被用于其他类型的对象 (比如类数组对象)。

避免多层嵌套,若条件符合则尽早 return

function test(fruit, quantity) {
  const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];

  // 条件 1: fruit 必须有值
  if (fruit) {
    // 条件 2: 必须是red的
    if (redFruits.includes(fruit)) {
      console.log('red');

      // 条件 3: quantity大于10
      if (quantity > 10) {
        console.log('big quantity');
      }
    }
  } else {
    throw new Error('No fruit!');
  }
}

缺点:嵌套语句过多,当你有很长的 if 语句时,你可能滚动到最底层才知道还有 else 语句。

function test(fruit, quantity) {
  const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];

  // 尽早抛出错误
  if (!fruit) throw new Error('No fruit!');

  if (redFruits.includes(fruit)) {
    console.log('red');

    if (quantity > 10) {
      console.log('big quantity');
    }
  }
}

优化方案:当发现无效语句时,尽早 return。可以用“倒置判断条件”进一步分离更多 if...else... 语句,如下:

function test(fruit, quantity) {
  const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];

  // 尽早抛出错误
  if (!fruit) throw new Error('No fruit!');

  // 当水果不是红色时停止继续执行(倒置判断)
  if (!redFruits.includes(fruit)) return; 

  console.log('red');

  if (quantity > 10) {
    console.log('big quantity');
  }
}

优化原则:在发现无效条件时,尽早 return。通过 “倒置判断条件 & 尽早return” 进一步减少if嵌套。
优点:少了一层嵌套语句,代码结构更加清晰。

应当尽力减少嵌套和尽早return,但不要过度。一两层的 if 语句嵌套在可接受范围之内,此时代码比较短且直接,包含if嵌套的更清晰,若无脑添加倒置判断条件可能加重思考的负担(增加认知载荷)。

使用默认参数和解构

在JavaScript中我们总是需要检查 null / undefined 的值和指定默认值。ES6 提供了函数参数的默认值写法,可以利用该特性对条件语句进行简化。

function test(fruit, quantity) {
  if (!fruit) return;
  // 如果 quantity 参数没有传入,设置默认值为 1
  const q = quantity || 1; 

  console.log(`We have ${q} ${fruit}!`);
}

我们可以通过声明 "默认函数参数" 来消除变量 q:

function test(fruit, quantity = 1) {
  // 如果 quantity 参数没有传入,设置默认值为 1
  if (!fruit) return;
  console.log(`We have ${quantity} ${fruit}!`);
}

当函数参数为 object 类型时,我们可以通过“对象解构”以及“函数默认参数”进行简化。

function test(fruit) { 
  // 当 fruit 值存在时打印 fruit 的 name 属性值,若不存在则打印 'unknown'
  if (fruit && fruit.name)  {
    console.log (fruit.name);
  } else {
    console.log('unknown');
  }
}

优化后:当 fruit 值不存在时,函数参数默认为 {}。此时 name 属性值为 undefined,对应布尔值为 false

// 解构 - 仅仅获取 name 属性
// 为其赋默认值为空对象
function test({name} = {}) {
  console.log (name || 'unknown');
}

此处还有一个优化技巧:简单的条件语句利用 &&|| 以及三元运算符来替代,参照 name || 'unknown' 的写法。

“对象映射”代替“条件语句”

function test(color) {
  // 使用条件语句来寻找对应颜色的水果
  switch (color) {
    case 'red':
      return ['apple', 'strawberry'];
    case 'yellow':
      return ['banana', 'pineapple'];
    case 'purple':
      return ['grape', 'plum'];
    default:
      return [];
  }
}

上述 switch 语句用对象遍历实现相同的结果,语法看起来更简洁:

// 判断条件的相关配置
const fruitColor = {
  red: () => ['apple', 'strawberry'],
  yellow:  () => ['banana', 'pineapple'],
  purple:  () => ['grape', 'plum']
};

fruitColor[color]() // 执行语句

对象映射的优势:分离配置信息(判断条件)与执行动作。
我们可定义一个 object 作为配置对象,存放不同状态,并通过链表进行查找。
对象映射可以将条件配置与具体执行分离。如果要增加其他状态,只修改配置对象即可(可灵活调整)。

const statusMap = {
    :()=>{
        console.log('a1')
    },
    2:()=>{
        console.log('b2')
    }
    /* n.... */
}
// 执行
let a = 1 
statusMap[a || 1]()

object 对象的 key 值只能是 string 类型,因此我们也可以使用 Map 替换 object,Map 可接受多种类型的 key 实现相同的结果:

const fruitColor = new Map()
  .set('red', () => ['apple', 'strawberry'])
  .set('yellow',  () => ['banana', 'pineapple'])
  .set('purple',  () => ['grape', 'plum']);

fruitColor[color]()

当然,有对象映射,对应也有数组映射,数组映射的 key 值为 0~n 的索引,具体使用依据业务场景进行选择。

对 "所有/部分" 判断使用 Array.prototype.every & Array.prototype.some

当判断条件为是否都满足 / 是否都不满足时,可以使用 Array.prototype.every
假设我们要检测所有水果是否都为红色,一般逻辑通过 for 循环依次判断来实现:

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

function test() {
  let isAllRed = true;

  // 条件:所有水果都是红色
  for (let f of fruits) {
    if (!isAllRed) break;
    isAllRed = (f.color == 'red');
  }

  console.log(isAllRed); // false
}

利用 JavaScript Array 的内置方法 Array.prototype.every(),我们可以减少代码行数:

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

function test() {
  // Array.prototype.every():遍历数组并对元素执行回调,回调返回一个 boolean 值,当所有返回的 boolean 值均为 true 是,整体返回 true。
  const isAllRed = fruits.every(f => f.color == 'red');
}

当判断条件为是否存在满足条件的情况 / 是否存在不满足条件时的情况,可以使用 Array.prototype.some
假设我们要检测是否有水果是红色的,只要存在一个即满足:

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

function test() {
  // 条件:任何一个水果是红色
  const isAnyRed = fruits.some(f => f.color == 'red');
}

总结

  • 避免多层条件语句嵌套:尽早 return,倒置条件判断。
  • 条件语句应避免“写死”:尽量用 “对象 / 数组 / Map” 等可动态调整的结构,即具有灵活性,又分离了逻辑条件与执行语句。
  • 巧用 Array 数组结构的内置方法:Array.prototype.includes()Array.prototype.some()Array.prototype.every() 等。

2022.06.29 补充

参考链接:JavaScript 复杂判断的更优雅写法

多元条件判断

原始代码:

/**
 * 按钮点击事件
 * @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
    }
  }
}

上述代码中,存在多层的条件判断嵌套关系

修改后的代码:

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

特点:
利用了 Map 对象:

  1. 可接纳任何类型作为 key 值,此处利用了正则表达式
  2. 实现了多元嵌套条件的字符串拼接以及重复逻辑复用
  3. 简单调用:利用数组循环的特性,符合正则条件的逻辑都会被执行,可以同时执行公共逻辑和单独逻辑。

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions