JavaScript 如何对大量的 if

您所在的位置:网站首页 if嵌套函数的使用方法有哪些呢 JavaScript 如何对大量的 if

JavaScript 如何对大量的 if

2024-07-14 08:52:04| 来源: 网络整理| 查看: 265

        当业务比较复杂,判断条件比较多,项目进度比较赶时,特别容易使用过多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类型,null、undefined和空字符串取反都为true,其余都为false。

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

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

undefined和null为false,只能表示不是undefined或null,并不能判断是否有元素和内容。任意数组,对象,函数(函数是特殊的对象)都转化为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 }

  || 对比 ??

  ||和??都是指定默认值

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

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

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

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

        而||是运算符左侧的值为null、undefined、0、''或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-value和Map来优化代码。

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'

        上面代码使用了?.运算符,直接在链式调用的时候判断,左侧的对象是否为null或undefined。如果是的,就不再往下运算,而是返回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不是null或undefined,但也不是函数,那么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种设计模式

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



【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭