当业务比较复杂,判断条件比较多,项目进度比较赶时,特别容易使用过多if-else。其弊端挺多的,如代码可读性差、代码混乱、复杂度高、影响开发效率、维护成本高等。

        因此在日常编程中,有必要采取一些措施,对一些if-else进行优化。

目录

什么是面条代码

if...if 型

else if...else if 型

重构策略

基本情形

查找表

责任链模式

if/else语句优化

排非策略

 ! 对比 !! 

三元运算符 c ? t : f

使用短路运算符 && 、|| 、

合并条件表达式

  || 对比 ??

单个if多条件优化

多个else if分支优化

switch

key-value

?.的说明

Map

责任链模式

策略模式+工厂方法


什么是面条代码

        在讲优化方案前先来了解常见的if-else结构类型。所谓的【面条代码】,常见于对复杂业务流程的处理中。它一般会满足这么几个特点:

  • 内容长
  • 结构乱
  • 嵌套深

        主流的编程语言均有函数或方法来组织代码。对于面条代码,不妨认为它就是满足这几个特征的函数吧。根据语言语义的区别,可以将它区分为两种基本类型:if...if 型、else if...else if 型。

if...if 型

        这种类型的代码结构形如:

function demo (a, b, c) {
  if (f(a, b, c)) {
    if (g(a, b, c)) {
      // ...
    }
    // ...
    if (h(a, b, c)) {
      // ...
    }
  }

  if (j(a, b, c)) {
    // ...
  }

  if (k(a, b, c)) {
    // ...
  }
}

        if-if-before

        它通过从上到下嵌套的 if,让单个函数内的控制流不停增长。不要以为控制流增长时,复杂度只会线性增加。我们知道函数处理的是数据,而每个 if 内一般都会有对数据的处理逻辑。那么,即便在不存在嵌套的情形下,如果有 3 段这样的 if,那么根据每个 if 是否执行,数据状态就有 2 ^ 3 = 8 种。如果有 6 段,那么状态就有 2 ^ 6 = 64 种。从而在项目规模扩大时,函数的调试难度会指数级上升。

else if...else if 型

        这个类型的代码控制流,同样是非常常见的。形如:

function demo (a, b, c) {
  if (f(a, b, c)) {
    if (g(a, b, c)) {
      // ...
    }
    // ...
    else if (h(a, b, c)) {
      // ...
    }
    // ...
  } else if (j(a, b, c)) {
    // ...
  } else if (k(a, b, c)) {
    // ...
  }
}

   else-if-before

    else if 最终只会走入其中的某一个分支,因此并不会出现上面组合爆炸的情形。但是,在深度嵌套时,复杂度同样不低。假设嵌套 3 层,每层存在 3 个 else if,那么这时就会出现 3 ^ 3 = 27 个出口。如果每种出口对应一种处理数据的方式,那么一个函数内封装这么多逻辑,也显然是违背单一职责原则的。并且,上述两种类型可以无缝组合,进一步增加复杂度,降低可读性。

        但为什么在这个有了各种先进的框架和类库的时代,还是经常会出现这样的代码呢?个人的观点是,复用的模块确实能够让我们少写【模板代码】,但业务本身无论再怎么封装,也是需要开发者去编写逻辑的。而即便是简单的 if else,也能让控制流的复杂度指数级上升。

重构策略

基本情形

        对看起来复杂度增长最快的 if...if 型面条代码,通过基本的函数即可将其拆分。下图中每个绿框代表拆分出的一个新函数:

         if-if-after: 

        不论控制流再复杂,函数体内代码的执行顺序也都是从上而下的。因此,我们完全有能力在不改变控制流逻辑的前提下,将一个单体的大函数,自上而下拆逐步分为多个小函数,而后逐个调用之。

        这种做法中所谓的不改变控制流逻辑,意味着改动并不需要更改业务逻辑的执行方式,只是简单地【把代码移出去,然后用函数包一层】而已。

        但是通过这种方式,我们能够把一个有 64 种状态的大函数,拆分为 6 个只返回 2 种不同状态的小函数,以及一个逐个调用它们的 main 函数。这样一来,每个函数复杂度的增长速度,就从指数级降低到了线性级。这样一来,我们就解决了 if...if 类型面条代码了。

查找表

        对于 else if...else if 类型的面条代码,一种最简单的重构策略是使用所谓的查找表。它通过键值对的形式(key-value)来封装每个 else if 中的逻辑。(具体例子代码见下文)

const rules = {
  x: function (a, b, c) { /* ... */ },
  y: function (a, b, c) { /* ... */ },
  z: function (a, b, c) { /* ... */ }
}

function demo (a, b, c) {
  const action = determineAction(a, b, c)
  return rules[action](a, b, c)
}

        每个 else if 中的逻辑都被改写为一个独立的函数,这时我们就能够将流程按照如下所示的方式拆分了:

         else-if-lookup

        对于先天支持反射的脚本语言来说,这也算是较为 trivial 的技巧了。但对于更复杂的 else if 条件,这种方式会重新把控制流的复杂度集中到处理【该走哪个分支】问题的 determineAction 中。

责任链模式

        在上文中,查找表是用键值对实现的,对于每个分支都是 else if (x === 'foo') 这样简单判断的情形时,'foo' 就可以作为重构后集合的键了。但如果每个 else if 分支都包含了复杂的条件判断,且其对执行的先后顺序有所要求,那么我们可以用职责链模式来更好地重构这样的逻辑。

        对 else if 而言,注意到每个分支其实是从上到下依次判断,最后仅走入其中一个的。这就意味着,我们可以通过存储【判定规则】的数组,来实现这种行为。如果规则匹配,那么就执行这条规则对应的分支。我们把这样的数组称为【责任链】

         else-if-chain:

        可以通过一个职责链数组来定义与 else if 完全等效的规则,具体代码例子见下文。

if/else语句优化

排非策略

        逻辑非(logic NOT),是逻辑运算中的一种,就是指本来值的反值。

示例一:

// 优化前
// 判断是否为空
if(value === null || value === NaN || value === 0 || value === ''|| value === undefined)
{
  ……
}

// 优化后
if(!value) {
  ……
}

 示例二:

// 优化前
// 判断是否数组是否含有符合某条件的元素
const name = arr.find(item => item.status === 'error')?.name
if(name !== undefined && name !== '') {
  ……
}

// 优化后
if(!!arr.find(item => item.status === 'error')?.name) {
  ……
}

 示例三:提前return,去除不必要的else

// 优化前
if (user && password) {
    // 逻辑处理
} else {
    throw('用户名和密码不能为空!')
}


// 优化后
if (!user || !password) return throw('用户名和密码不能为空!') // 逻辑处理

 ! 对比 !! 

        !可将变量转换成boolean类型,nullundefined和空字符串取反都为true,其余都为false

!null=true
!undefined=true
!''=true
!100=false
!'abc'=false

         !! 常常用来做类型判断。

  • undefinednullfalse,只能表示不是undefinednull,并不能判断是否有元素和内容。
  • 任意数组,对象,函数(函数是特殊的对象)都转化为true,即使是空数组,空对象。
  • 空字符串为false,非空字符串为true
  • 数值正负0,不确定值(NaN)为false,其它为true,无穷大也是true

        字符串'0'和数值0可以相互转换,但它们转换为不同的布尔值,即0可转换为'0''0'可转换为true,但0却转换为false,可见Javascript中类型转换不具有传递性。

	let a; // null、undefined、''、0
    if (a !== null && typeof(a) !== "undefined" && 
        a !== undefined && a !== '' && a !== 0){
          //a为truthy时才执行,或者说a为真值时才执行
       }

 完全等价于:

	let a;
	if(!!a){
	//a为truthy时才执行,或者说a为真值时才执行
	}

!与 !!对比:

// 示例一:
var temp = null
alert(temp) // null
alert(!temp) // true
alert(!!temp) // false

// 示例二:
var temp
alert(temp) // undefined
alert(!temp) // true
alert(!!temp) // false

// 示例三:
var temp = ''
alert(temp) // 空
alert(!temp) // true
alert(!!temp) // false

// 示例四:
var temp = 1
alert(temp) // 1
alert(!temp) // false
alert(!!temp) // true

// 示例五:
var temp = 0
alert(temp) // 0
alert(!temp) // true
alert(!!temp) // false

// 示例六:
var temp = 'abc'
alert(temp) // abc
alert(!temp) // false
alert(!!temp) // true

// 示例七:
var temp = [1, 2]
alert(temp) // 1,2
alert(!temp) // false
alert(!!temp) // true

// 示例八:
var temp = { color: "#A60000", "font-weight": "bold" }
alert(temp) // [object: Object]
alert(!temp) // false
alert(!!temp) // true

三元运算符 c ? t : f

        三元运算符: condition ? exprIfTrue : exprIfFalse; 如果条件为真值,则执行冒号(:)前的表达式;若条件为假值,则执行最后的表达式。

示例一:

// 优化前
let allow = null
if(age >= 18){ 
   allow = '通过'; 
} else { 
   allow = '拒绝'; 
}

// 优化后
let allow = age >= 18 ? '通过' : '拒绝'

示例二:

// 优化前
if (flag) {
 success();
} else {
 fail();
}
  
//优化后
flag ? success() : fail();

        三元运算符相比if/else来说,只需一行语句,代码简练精炼。

使用短路运算符 && 、|| 、

  • && 为取假运算,从左到右依次判断,如果遇到一个假值,就返回假值,以后不再执行,否则返回最后一个真值;
  • || 为取真运算,从左到右依次判断,如果遇到一个真值,就返回真值,以后不再执行,否则返回最后一个假值。
// 优化前
if (flag) {
 this.handleFn() // handleFn是普通函数
}

// 优化后
flag && this.handleFn()

        这种写法就比较清晰,简洁,好读。

合并条件表达式

        另外如果遇到有很多的if语句,但是执行的功能函数却是一致的情况,可以用”逻辑与“(&&)或者”逻辑或“(||)来把他们合并成一个表达式。如果这些彼此独立的条件判断可以被视为同一次检查的场景时,一次检查的意图明显在可读性上优于多次的条件检查。例如:

// 优化前
if (!(staffInfo.patientName && staffInfo.phone)) {
  // doSomething
}
...
if (!(staffInfo.phone && staffInfo.idCardNum)) {
  // doSomething
}


// 优化后
if(!(staffInfo.patientName && staffInfo.phone) || !(staffInfo.phone && staffInfo.idCardNum)){
  // doSomething
} 

  || 对比 ??

  ||??都是指定默认值

        读取对象属性的时候,如果某个属性的值是nullundefined,有时候需要为它们指定默认值。常见做法是通过||运算符指定默认值。

const headerText = response.settings.headerText || 'Hello, world!';
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;

        上面的三行代码都通过||运算符指定默认值,但是这样写是错的。开发者的原意是,只要属性的值为nullundefined,默认值就会生效,但是属性的值如果为空字符串或false0,默认值也会生效。

        为了避免这种情况,ES2020 引入了一个新的 Null 判断运算符??。它的行为类似||,但是只有运算符左侧的值为nullundefined时,才会返回右侧的值。

        ||是运算符左侧的值为nullundefined0''NaN时,都会返回右侧的值。

单个if多条件优化

// 优化前
function test(type) {
  if (type === 'jpg' || type === 'png' || type === 'gif' || type === 'svg') {
    console.log("该文件为图片");
  }
}


// 优化后
function test(type) {
    const imgArr = ['jpg', 'png', 'gif', 'svg']
    if (imgArr.includes(type)) {
        console.log("该文件为图片")
    }
}

多个else if分支优化

        多个else if通常是一个糟糕的选择,它导致设计复杂,代码可读性差,并且可能导致重构困难。

// 示例一:优化前
if (this.type === 'A') {
  this.handleA();
} else if (this.type === 'B') {
  this.handleB();
} else if (this.type === 'C') {
  this.handleC();
} else if (this.type === 'D') {
  this.handleD();
} else {
  this.handleE();
}

// 示例二:优化前
function getTranslation(type) {
  if (type === 4) {
    return "forbidden_area";
  } else if (type === 6) {
    return "elevator_area";
  } else if (type === 7) {
    return "dangerous_area";
  } else if (type === 10) {
    return "restricted_area";
  }
  return "other_area";
}
// 示例三:优化前
    let result;
    if (type === 'add'){
    result = a + b;
    } else if(type === 'subtract'){
    result = a - b;
    } else if(type === 'multiply'){
    result = a * b;
    } else if(type === 'divide'){
    result = a / b;
    } else {
    console.log('Calculation is not recognized');
    }

        不同条件分支的代码具有很高的耦合度,先前的条件判断将影响后续的代码流,并且此类代码在后续开发中难以维护。我们可以通过switch、key-valueMap来优化代码。

switch

         我们可以通过使用 switch 语句优化,如下所示:

// 示例一:
// switch 优化后
  switch(val){
    case 'A':
      handleA()
      break
    case 'B':
      handleB()
      break
    case 'C':
      handleC()
      break
    case 'D':
      handleD()
      break
  }
// 示例二:
// switch 优化后
function getTranslation(type) {
  switch (type) {
    case 4:
      return "forbidden_area";
    case 6:
      return "elevator_area";
    case 7:
      return "dangerous_area";
    case 10:
      return "restricted_area";
    default:
      return "other_area";
  }
}

// 示例三:
// switch 优化后
let result;
switch (type) {
   case 'add':
     result = a + b;
     break;
   case 'subtract':
     result = a - b;
     break;
   case 'multiply':
     result = a * b;
     break;
   case 'divide':
     result = a / b;
     break;
   default:
    console.log('Calculation is not recognized');
}

        但这仍然没有什么可读性。switch 语句也容易出错。

        在这种情况下,我们只是返回一个值,但是当你具有更复杂的功能时,很容易错过 break 语句并引入错误。

key-value

        虽然switch语句在逻辑上确实比else if语句简单,但是代码本身也有点多。 其实可以对象枚举,将条件与特定操作相关联的键值。

// 示例一:
// key-value优化后
let enums = {
  'A': handleA,
  'B': handleB,
  'C': handleC,
  'D': handleD,
  'E': handleE
}

function action(val){
  let handleType = enums[val]
  handleType()
}

        这种方式消除了所有条件语句,并使用键值对象存储条件和操作之间的关系。当我们需要根据条件执行代码时,我们不再需要使用else if或switch语句来处理相应的动作,我们只需从中提取相应的函数handleType并执行它即可。

// 示例二:
// key-value优化后
function getTranslation(type) {
  const types = {
    4: 'forbidden_area',
    6: 'elevator_area',
    7: 'dangerous_area',
    10: 'restricted_area'
  }
  return types[type] ?? 'other_area'
}

        有时你可能需要在你的条件中执行一些更复杂的逻辑。为此,你可以将函数作为值传递给对象键并执行响应

// 示例三:处理更复杂的逻辑
function calculate(action, num1, num2) {
  const actions = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b,
    multiply: (a, b) => a * b,
    divide: (a, b) => a / b,
  };
​
  return actions[action]?.(num1, num2) ?? "Calculation is not recognised";
}

   ?.有不懂的话,可以先看下面,我们正在选择我们想要做的计算并执行响应,传递两个数字。你可以使用可选链接(最后一行代码中的?.)来仅执行已定义的响应。否则,将使用默认的返回字符串。

        如果函数里的逻辑足够复杂也可以把函数提取出来:

// 把函数提取出来
function add(num1, num2) {
  return num1 + num2
}
function subtract(num1, num2) {
  return num1 - num2
}
function multiply(num1, num2) {
  return num1 * num2
}
function divide(num1, num2) {
  return num1 / num2
}
function calculate(action, num1, num2) {
  const actions = {
    add,
    subtract,
    multiply,
    divide
  }
​
  return actions[action]?.(num1, num2) ?? 'Calculation is not recognised'
}

         定义相关函数拆分逻辑,简化代码举例:

// 定义相关函数拆分逻辑,简化代码
// 优化前
function itemDropped(item, location) {
    if (!item) {
        return false;
    } else if (outOfBounds(location) {
        var error = outOfBounds;
        server.notify(item, error);
        items.resetAll();
        return false;
    } else {
        animateCanvas();
        server.notify(item, location);
        return true;
    }
}


// 优化后
// 定义dropOut和dropIn, 拆分逻辑并提高代码可读性
function itemDropped(item, location) {
  const dropOut = function () {
    server.notify(item, outOfBounds);
    items.resetAll();
    return false;
  };

  const dropIn = function () {
    animateCanvas();
    server.notify(item, location);
    return true;
  };

  return !!item && (outOfBounds(location) ? dropOut() : dropIn());
}

         细心的朋友会发现,在这个例子中,同时使用了前文提及的优化方案。这说明在编码时可以根据实际情况混合使用多种解决方案

?.的说明

        编程实务中,如果读取对象内部的某个属性,往往需要判断一下,属性的上层对象是否存在。比如,读取 message.body.user.firstName 这个属性,安全的写法是写成下面这样:

// 错误的写法
const  firstName = message.body.user.firstName || 'default';
​
// 正确的写法
const firstName = (message
  && message.body
  && message.body.user
  && message.body.user.firstName) || 'default';

        上面例子中,firstName属性在对象的第四层,所以需要判断四次,每一层是否有值。

        这样的层层判断非常麻烦,因此 ES2020 引入了“链判断运算符”(optional chaining operator)?.,简化上面的写法。

// 简化后的写法
const firstName = message?.body?.user?.firstName || 'default'

        上面代码使用了?.运算符直接在链式调用的时候判断,左侧的对象是否为nullundefined。如果是的,就不再往下运算,而是返回undefined

        下面是判断对象方法是否存在,如果存在就立即执行的例子。

iterator.return?.()

        上面代码中,iterator.return 如果有定义,就会调用该方法,否则 iterator.return 直接返回undefined,不再执行?.后面的部分。

        下面是?.运算符常见形式,以及不使用该运算符时的等价形式。

a?.b
// 等同于
a == null ? undefined : a.b
​
a?.[x]
// 等同于
a == null ? undefined : a[x]
​
a?.b()
// 等同于
a == null ? undefined : a.b()
​
a?.()
// 等同于
a == null ? undefined : a()

        上面代码中,特别注意后两种形式,a?.b()a?.()

        如果a?.b()里面的a.b有值,但不是函数,不可调用,那么a?.b()是会报错的。

  a?.()也是如此,如果a不是nullundefined,但也不是函数,那么a?.()会报错。

Map

        实际上我们还可以通过Map来进一步的优化我们的代码。

        对比Object的话,Map具有许多优点:

  • 对象的键只能是字符串或符号,而Map的键可以是任何类型的值。它可以是一个对象、数组或者更多类型,更加灵活。
  • 我们可以使用Map size属性轻松获取Map的键/值对的数量,而对象的键/值对的数量只能手动确定。
  • 具有极快的查找速度。

上面的例子一可以优化如下:

// 示例一:
let enums = new Map([
  ['A', handleA],
  ['B', handleB],
  ['C', handleC],
  ['D', handleD],
  ['E', handleE]
])

function action(val){
  let handleType = enums.get(val)
  handleType()
}

        如果我们遇到多层复杂条件,Map语句优势就更明显了:

let statusMap = new Map([
  [
    { role: "打工人", status: "1" },
    () => { /*一些操作*/},
  ],
  [
    { role: "打工人", status: "2" },
    () => { /*一些操作*/},
  ],
  [
    { role: "老板娘", status: "1" },
    () => { /*一些操作*/},
  ],
])

let getStatus = function (role, status) {
  statusMap.forEach((value, key) => {
    // JSON.stringify()可用于深比较/深拷贝
    if (JSON.stringify(key) === JSON.stringify({ role, status })) {
      value()
    }
  })
}

getStatus("打工人", "1"); // 一些操作

再复杂一点:

// 优化前
if (mode == 'kline') {
    if (this.type === 'A') {
        this.handleA()
    } else if (this.type === 'B') {
        this.handleB()
    } else if (this.type === 'C') {
        this.handleC()
    } else if (this.type === 'D') {
        this.handleD()
    }
} else if ((mode = 'depth')) {
    if (this.type === 'A') {
        this.handleA()
    } else if (this.type === 'B') {
        this.handleB()
    } else if (this.type === 'C') {
        this.handleC()
    } else if (this.type === 'D') {
        this.handleD()
    }
}

        对于上述如此复杂的场景,是否可以通过Map来进行优化? 其实只需要将不同的判断语句连接成一个字符串,以便可以将条件和操作以键值格式关联在一起

// 优化后
let enums = new Map([
  ['kline_A', handleKlineA],
  ['kline_B', handleKlineB],
  ['kline_C', handleKlineC],
  ['kline_D', handleKlineD],
  ['kline_E', handleKlineE],
  ['depth_A', handleDepthA],
  ['depth_B', handleDepthB],
  ['depth_C', handleDepthC],
])

function action(mode, type){
  let key = `${mode}_${type}`
  let handleType = enums.get(key)
  handleType()
}

责任链模式

        责任链模式:将整个处理的逻辑改写成一条责任传递链,请求在这条链上传递,直到有一个对象处理这个请求。

        例如 JS 中的事件冒泡。简单来说,事件冒泡就是在一个对象上绑定事件,如果定义了事件的处理程序,就会调用处理程序。相反没有定义的话,这个事件会向对象的父级传播,直到事件被执行,最后到达最外层,document对象上。

         这意味着,在这种模式下,总会有程序处理该事件。

// 优化前
function demo (a, b, c) {
  if (f(a, b, c)) {
    if (g(a, b, c)) {
      // ...
    }
    // ...
    else if (h(a, b, c)) {
      // ...
    }
    // ...
  } else if (j(a, b, c)) {
    // ...
  } else if (k(a, b, c)) {
    // ...
  }
}


// 优化后
// 可以通过一个职责链数组来定义与 else if 完全等效的规则
// rules 中的每一项都具有 match 与 action 属性。这时我们可以将原有函数的 else if 改写对职责链数组的遍历:


const rules = [
  {
    match: function (a, b, c) { /* ... */ },
    action: function (a, b, c) { /* ... */ }
  },
  {
    match: function (a, b, c) { /* ... */ },
    action: function (a, b, c) { /* ... */ }
  },
  {
    match: function (a, b, c) { /* ... */ },
    action: function (a, b, c) { /* ... */ }
  }
  // ...
]

// 每个职责一旦匹配,原函数就会直接返回。
// 这也完全符合 else if 的语义。
// 通过这种方式,我们就实现了对单体复杂 else if 逻辑的拆分了。
function demo (a, b, c) {
  for (let i = 0; i < rules.length; i++) {
    if (rules[i].match(a, b, c)) {
      return rules[i].action(a, b, c)
    }
  }
}

策略模式+工厂方法

        此法比较复杂,感兴趣的可以去学习一下。

引用文章:

如何无痛降低 if else 面条代码复杂度 - 掘金

JS中if/else的优化 - 掘金

如何替换项目中的if-else和switch - 掘金

如何避免使用过多的 if else? - 掘金

如何 “干掉” if...else - 简书

6个实例详解如何把if-else代码重构成高质量代码_if else c重构_yinnnnnnn的博客-CSDN博客

设计模式目录:22种设计模式

责任链设计模式(职责链模式)

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐