手把手教你写出优雅的条件判断

1,309 阅读10分钟

取值的Bug

本文开始前先分享一些平时写代码中比较容易错的代码片段,大家可以看看自己日常工作中有没有写出过下面这些代码导致程序经常出错呢。随着ES版本不断的更新,ES6+编写JS方式几乎普遍运用于前端项目中了,也很少在代码中看到vararguments..等影子,不得不说ES6+编写代码确实能够帮助我们规避很多不容易察觉的坑,也让我们写的代码更加精简,不过享受后者带给我们的方便同时我们也要注意下细节,这样才能更好的让自己代码写的即简洁又健壮

解构赋值

嗯哼🤔?解构赋值也还有不会的??

res.js

// => 先给份数据当作请求接口返回的数据
let res = {
  data: {
    name: '前端自学驿站',
    weChat:'itBeige',
    public: '前端自学驿站',
    slogan: '驿路向北 白折不回',
    obj1: {id: '01', a: 1, b: 2, children: [{a: 'c1'}, {b: 'c2'}]},
    journalizing: [
      {id: '01', type: '1'},  
      {id: '02', type: '2'}, 
      {id: '03', type: '3'},
      {id: '03', type: '1'},
    ]
  }
}

export {
    res
}

解构赋值代码中必出现,多层级解构也是很常用的

let {
   obj1: {
     id,
     a,
     children: c,
   },
   obj1: obj2
} = res.data
console.log(obj1, obj2);

A: 报错 B: obj1 和obj2一样 C: 两者不一致

// 从接口返回的数据根据type分割成两个数组
let contractType = ['1']
let list2 = []
let {
  data: {journalizing} = []
} = res
for (let i = 0, len = journalizing.length; i < len; i++) {
  let row = journalizing[i]
  if (contractType.includes(row.type)) {
    list2.push(journalizing.splice(i, 1))
  }
}
console.log(list2); // ?

这块就是len = journalizing.leng的问题,在之前接手的项目看到一块代码写了个for循环来处理后端返回数据,是涉及到将编码(H:20:18:3:1:2)根据不同编码节点放到对应数组项,比上面的要复杂,逻辑都写的挺好的,就是当时定义出来len = arr.length之后动过数组,忽略了len声明定义的基础类型值在arr变动之后len还是最开始的数组长度

import res form './res.js'
new Vue({
  el: '#app1',
  data() {
    return {
      list: []
    }
  },
  computed: {
   contractList({list, drawTypeToList}) {
    return drawTypeToList(list, '1')
   },
   planList({list, drawTypeToList}) {
    return drawTypeToList(list, '2')
   },
   subjectList({list, drawTypeToList}) {
    return drawTypeToList(list, '3')
   },
  },
  created () {
    let {
      contractList,
      planList,
      subjectList,
    } = this
    this.list = res.data.journalizing
    console.log(contractList.length); // ?
  },
  methods: {
    drawTypeToList(list, type) {
      return list.filter(i => i.type === type)
    }
  }
})

我们在日常写代码的时候一般都会把定义变量放到方法最上面:因为let不存在变量提升机制,也因为方法一般都会有if/else判断语句的存在,所以变量放在最上面是合理的,这里需要注意的就是计算属性他也只是个属性,虽然返回引用类型的值,但注意在created钩子函数执行的时候解构出来的变量是 list.filter()返回的数组,后面list变动后重新计算又生成了一个新数组对象

代码演示

 data () {
    return {
      list: [] // 0xAAFF00
    }
  }

  /*  计算属性
    contractList({list, drawTypeToList}) {
     return drawTypeToList(list, '1') -> 0xAAFF11
    },
    planList({list, drawTypeToList}) {
     return drawTypeToList(list, '2') -> 0xAAFF22
    },
    subjectList({list, drawTypeToList}) {
     return drawTypeToList(list, '3') -> 0xAAFF33
    },
 */

  /* methods */
  /* 
    drawTypeToList(list, type) { list => 0xAAFF00
      // 第一次返回:0xAAFF11
      // 第二次返回:0xAAFF22
      // 第三次返回:0xAAFF33
      return list.filter(i => i.type === type) 
    }
  */
  let {
    contractList, // 0xAAFF11
    planList, // 0xAAFF22
    subjectList, // 0xAAFF33
  } = this
  this.list = res.data.journalizing
  // list 变动计算属性重新计算 -> 函数重新调用 -> 计算属性重新返回三个新的引用类型地址
let calc = {
  Add(a = 0, b = 0) {
    return a + b
  }
}
calc.Add('', 1) // ?
calc.Add(null, 2) // ?
calc.Add(undefined, 3) // ?

手摸手教你写出优雅的条件判断

8.jpg

当你的业务逻辑足够复杂的时候,你的代码就不能光想着if/else/switch条件判断了: 时间一久亦或者是后面的人接手你的代码看到这块逻辑的时候理解起来就要半天,下面我将给出几个相对复杂场景下的条件处理逻辑

合计

表格合计相信大家都用过的,summary-method回调方法传递表格数据,返回数组(索引对应column索引)来返回对应列的合计

现在的场景是这样的,数组中涉及到:

  • 普通的字段当前列所有数据相加就完事
  • 需要计算得到的数据:
    • 比如差异值c,需要 a - b
    • 还有一个较为特殊的,2021年份下对应12个月份,将12个月拆成4个季度值字段

1.png

数据

data.js

let data = [ // property肯定是乱序的
  {
    contractAmountTax: 5378,
    estimatedSettlementAmt: 2461,
    jan: 1,
    feb: 2,
    mar: 3,
    cumulativePaymentAppAmt: 323,
    cumulativePaymentAmt: 1284,
    jun: 6,
    jul: 7,
    planPayAmount: 6801,
    apr: 4,
    may: 5,
    aug: 8,
    sep: 9,
    oct: 10,
    nov: 12,
    dec: 12,
  }
]

前端需要展示除了月份,还有月份对应的4个季度, 及12个月份汇总当前年份的合计, 及有些字段是需要计算出来的, 比如: PayButRec = estimatedSettlementAmt - cumulativePaymentAppAmt

实现普通求值的合计

这个毫无疑问肯定是要写判断的, 其难度并不高但你需要去思考下怎么样设计才能让你少写一些冗杂的判断语句, 下面我将手把手实现这个需求

// 季度对应的月份
const quarterGetByMonth = {
  firstQuarter: ['jan', 'feb', 'mar'],
  secondQuarter: ['apr', 'may', 'jun'],
  thirdQuarter: ['jul', 'aug', 'sep'],
  fourthQuarter: ['oct', 'nov', 'dec'],
}

/* 普通求值字段 */
const planFieldHash = {
  contractAmountTax: -1,
  estimatedSettlementAmt: -1,
  cumulativePaymentAppAmt: -1,
  cumulativePaymentAmt: -1,
  planPayAmount: -1,
}

/* 计算出来的字段值 */
const calcFieldHash = {
  // estimatedSettlementAmt - cumulativePaymentAppAmt
  PayButRec: -1, 
}

 /* 多个值汇总计算出来的字段值 */
 const gatherFieldHash = {
  firstQuarter: -1,
  secondQuarter: -1,
  thirdQuarter: -1,
  fourthQuarter: -1,
}

el-table传入prop:summary-method: getSummaries

function getSummaries(param) {
  const { columns, data } = param;
  const sums = [];

  columns.forEach((column, index) => {
    if (index === 0) {
      sums[index] = "合计";
      return;
    }
    
    // 获取普通求值字段对应索引
    if (planFieldHash.hasOwnProperty(column.property)) {
      planFieldHash[column.property] = index
      return;
    }

    // 获取计算求值字段对应索引
    if (calcFieldHash.hasOwnProperty(column.property)) {
      planFieldHash[column.property] = index
      return;
    }
    
    // 获取季度列所在的索引
    if (quarterGetByMonth.hasOwnProperty(column.property)) {
      gatherFieldHash[column.property] = index
      return;
    }

    /* 如果还有其他特殊合计值继续往下走逻辑 */
  });

  
  // 做一层过滤,如果值为-1索引表示没有这个字段(动态列会出现这种情况)
  let filterNegative = (hash) => {
    return Object.entries(hash).reduce((keyValObj, [key, index]) => {
      if (index !== -1) {
        keyValObj[key] = index
      }
      return keyValObj
    }, {})
  }

  let planKeyValObj = filterNegative(planFieldHash)
  let calcKeyValObj = filterNegative(calcFieldHash)
  let gatherKeyValObj = filterNegative(gatherFieldHash)
  /* planKeyValObj: {contractAmountTax: 3, estimatedSettlementAmt: 6 ....} */

  let planProps = Object.keys(planKeyValObj)
  let calcProps = Object.keys(calcKeyValObj)
  let gatherProps = Object.keys(gatherKeyValObj)
  /* planProps: [contractAmountTax, estimatedSettlementAmt, ....] */


  /* 普通合计逻辑 */
  if (planProps.length) {
    /**
     * calcArrayTotal方法接受三个参数:
     * data: 求值的数组
     * planProps: 要求值的字段数组
     * calc.Add: 计算函数(相加/乘/除....)
     * 
     * return 数组中所有字段值的汇总, 举个例子
     * data: [{a: 1, b: 2, c: 3}, {a: 11, b: 22, c: 33}]
     * planProps: [a, c]
     * calc.Add: 相加操作
     * return {a: 12, c: 36}
     * 
     * 或者 calc.Mul: 相减操作
     * return {a: -10, c: -30}
     */
    let planTotalObj = calcArrayTotal(data, planProps, calc.Add)
    planProps.forEach(prop => {
      let sumIndex = planKeyValObj[prop] // 当前字段的index
      let totalValue = planTotalObj[prop] // 当前字段的合计值
      // 支持负数千分符
      let ret = totalValue.toFixed(2).replace('-', '') // 去了负号的数
      /* formatThouPercentile: 将数字添加千分号 */
      sums[sumIndex] = Number(ret) > 999 ? formatThouPercentile(totalValue) : totalValue
    })
  }

  return sums;
}

验证一下

let sums = getSummaries({
  data,
  columns: [ // 偷个懒
    { property: 'index'}, 
    {property: 'PayButRec'}, 
    ...Object.keys(data[0]).map(property => ({property}))
  ] 
})

2.png

实现季度的合计

接着上面实现下季度的合计

if (gatherProps.length) {
  calcQuartersSummary(data, quarterGetByMonths, calc.Add)
  let gatherTotalObj = calcArrayTotal(data, gatherProps, calc.Add)
  gatherProps.forEach(prop => {
    let sumIndex = gatherKeyValObj[prop] // 当前字段的index
    let totalValue = gatherTotalObj[prop] // 当前字段的合计值
    // 支持负数千分符
    let ret = totalValue.toFixed(2).replace('-', '') // 去了负号的数
    /* formatThouPercentile: 将数字添加千分号 */
    sums[sumIndex] = Number(ret) > 999 ? formatThouPercentile(totalValue) : totalValue
  })
}

function calcQuartersSummary(list = [], quarterGetByMonths, operate) {
  list.forEach(row => {
    let curRowQuartersVal = Object.entries(quarterGetByMonths).reduce((quarterObj, [quarter, months]) => {
      quarterObj[quarter] = months.reduce((total, m) => operate(row[m] || 0, total), 0)
      return quarterObj
    }, {})
    Object.assign(row, curRowQuartersVal)
  })
}

验证一下

let sums = getSummaries({
  data,
  columns: [ // 继续偷懒
    { property: 'index'}, 
    {property: 'PayButRec'}, 
    ...Object.keys(quarterGetByMonths).map(property => ({property})),
    ...Object.keys(data[0]).map(property => ({property}))
  ] 
})

3.png

还剩一个差异值的合计过于简单,这里就不多贴代码了

业务中常见的条件判断

接下来再看一个业务中很典型的条件判断

4.gif

/**
	上图是一个很典型的左树右表, 在上图中我们知道: 集团 -> 多个经营公司 -> 公司下存在多个项目
	公司的表单项和项目的的略有差异, 这个时候我们最少需要去做4种情况的判断来调用接口:
	公司: 新增/修改
	项目: 新增/修改
	表单的标题也同上
*/

对于上面这种需求难度也是不大,只不过需要去做过多的条件判断,当前表单是处于公司Or项目节点,是修改Or新增操作,这个时候我们也可以稍微设计一下,避免去写过多的条件语句后面看代码让自己也看的舒服一点。

// 不必要的APi这里就不写了
import {
  addCompany, // 新增经营公司
  putCompany, // 修改经营公司
  addProject, // 新增经营公司下的项目
  putProject, // 修改经营公司下的项目
} from '@/api/tenancy/base-data/projectAPI'

// 操作的类型
const isViewCompany = { isProject: false, isEdit: false }
const isPutCompany = { isProject: false, isEdit: true }
const isAddCompany = { isProject: false, isAdd: true }
const isViewProject = { isProject: true, isEdit: false }
const isPutProject = { isProject: true, isEdit: true }
const isProject = { isProject: true, isAdd: true }

// 接口Hash表
const joggleMap = new Map([
  [isViewCompany, addCompany], // 添加经营公司
  [isPutCompany, putCompany], // 修改经营公司
  [isViewProject, addProject], // 添加项目
  [isPutProject, putProject], // 修改项目
])

// 6种类型对应的默认表单配置项
const FormConfigMap = new Map([
  [isViewCompany, { title: '经营公司-查看' }],
  [isPutCompany, { title: '经营公司-修改' }],
  [isAddCompany, { title: '经营公司-增加' }],
  [isViewProject, { title: '项目-查看' }],
  [isPutCompany, { title: '项目-修改' }],
  [isProject, { title: '项目-增加' }],
])

看到上面这些相信小伙伴们就能Get到我的点了。

通过条件来匹配对应的表单配置项

@click.stop="handleNodeClick(node, data)"点击节点时触发函数, node当前节点信息,data是当前节点数据

// 查看节点操作
handleNodeClick(node, data) {
 // 点击最外层集团不做任何展示
 if (node.level === 1) return

 const defaultVal = [...FormConfigMap].find(([operate]) => {
   // 通过判断当前操作项: 必须是查看,通过判断节点登记来判断是点击了公司Or项目
   // 公司节点:3, 项目节点:2
   return isEqual(operate, { isEdit: false, isProject: node.level === 3 })
 })?.reduce((options, arr) => Object.assign(options, arr), {})

 // 最后将匹配到的配置项融合到默认配置项里面
}

就上面这些代码就实现了,几乎没有出现条件语句就实现了右侧表单的6种标题判断

接下来就是在点击保存的时候我们需要判断当前是处于什么节点,是否是新增Or编辑来调接口,这种也是业务中特别常见,我们通过几个变量来表示当前的操作,然后if/ else 里面继续if/else就能实现了,但我这里给个不一样的思想,同样还是通过设计来优化我们的条件判断

save() {
  const {
    isEdit, // 是否是编辑
    isProject // 是否是项目节点
	} = this 
  const joggle = [...joggleMap].find(([key, value]) => isEqual(key, { isEdit, isProject }))?.[1]
  const res = await joggle(this.form)
}

还是通过事先设计的4种操作对应4个接口的方式,来匹配出对应操作的接口,最后去调用匹配的接口。

逻辑抽象能力

如果上面那些你能看明白的话,那么我相信你肯定多少能学习到一些东西,现在就让我们一起来测试下你的逻辑抽象能力有多强,这对于一个工程师对说是非常重要的!

let data = {
  a: {
    count: [1, 2, 3, 4, 5, 6, 7, 8, 9]
  },
  b: {
    count: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
  },
  c: {
    count: [16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
  }
}

setValToState(config, this.operate, this.state)
function setValToState(config, operate, state) {
  if (operate === 'insert') {
    if (state === 1 || state === 2) {
      console.log('output 2'); // output 
      data.count['a'].push(3) // input 
    } else if (state === 3 || state === 4) {
      console.log('output 4'); // output 
      data.count['a'].push(5) // input 
    } else if (state === 5 || state === 6) {
      console.log('output 6'); // output 
      data.count['a'].push(7) // input 
    } else if (state === 7 || state === 8) {
      console.log('output 8'); // output 
      data.count['a'].push(9) // input 
    }
  } else if (operate === 'put') {
    if (state === 1 || state === 2) {
      data.count['b'].splice(3, 1, 6)
    } else if (state === 2 || state === 3) {
      data.count['b'].splice(5, 1, 10)
    } else if (state === 3 || state === 4) {
      data.count['b'].splice(7, 1, 14)
    } else if (state === 4 || state === 5) {
      data.count['b'].splice(9, 1, 18)
    }
  } else if (operate === 'remove') {
    if (state === 1 || state === 2) {
       data.count['c'].splice(1, 2)
    } else if (state === 2 || state === 3) {
      data.count['c'].splice(1, 3)
    } else if (state === 3 || state === 4) {
      data.count['c'].splice(1, 4)
    } else if (state === 4 || state === 5) {
      data.count['c'].splice(1, 5)
    }
  }
}

思考一下,你会怎么去将这种硬编码方式进行逻辑抽象呢?

------------------------- 思考一下再来看 ----------------------------

inset的实现

4.png

put的实现

5.png

remove的实现

6.png

实现逻辑

定义conditionActions

let conditionActions = Object.create(
  {
    action: (state, config) => {
      for (const [output, includeStates, val] of config) {
        if (includeStates.includes(state)) {
          return { output, val}
        };
      }
    }
  }, 
  {
    insert: {
      writable: false,
      value: (output, val, data) => {
        console.log(`output ${output}`); // output 
        data['a'].count.push(val) // input 
      }
    },
    put: {
      writable: false,
      value: ({index, length} = {}, val, data) => {
      	data['b'].count.splice(index, length, ...val)
    	}
    },
    remove: {
      writable: false,
      value: ({index, length} = {}, val, data) => {
      	data['b'].count.splice(index, length)
    	}
    },
  }
)

定义configHash

let configHash = {
  insert: [
    [2, [1, 2], 3],
    [4, [2, 3], 5],
    [6, [3, 4], 7],
    [8, [4, 5], 9],
  ],

  put: [
    [{index: 3, length: 2}, [1, 2], [6, 7]],
    [{index: 5, length: 3}, [2, 3], [6, 7, 8]],
    [{index: 7, length: 4}, [3, 4], [6, 7, 8, 9]],
    [{index: 9, length: 5}, [4, 5], [6, 7, 8, 9, 10]],
  ],

  remove: [
    [{index: 2, length: 4}, [1, 2]],
    [{index: 1, length: 1}, [2, 3]],
    [{index: 3, length: 3}, [3, 4]],
    [{index: 6, length: 2}, [4, 5]],
  ],
}
// state -> 3  operate -> 'insert' data -> 往🖕滑定义过data数据
function actionsMethod(state, operate, data) { 
  let curOperate = Reflect.get(conditionActions, operate)
  if (!curOperate) return
  let retObj = conditionActions.action(state, configHash[curOperate])
  if (Object.prototype.toString.call(retObj) === '[object Object]') {
    curOperate(retObj.output, retObj.val, data)
  }
}

到这也就差不多没了, 以上都是本人在项目中真实的应用场景,对于大部分前端来说其实80%都是在写业务代码,而我们可以在写代码的过程中去不断的思考相同逻辑的代码能不能用更好的方式写出来呢?抱着这样的心态,我相信在就算是搬砖也会有点小收获的,而不是一点意义都没有的重复性工作。

比如下面这点:

9.gif

/**
  * @description: 增加行
  * @param {Boolean} type:1 -> 证照信息 2 -> 银行信息  3 -> 联系人信息
  * @return {*}
  * @Date Changed:
*/
insertRow(type) {
  let Ref = null
  let activeField = null
  switch (type) {
  case 1:
    Ref = 'attchemendTableRef'
    activeField = 'documentType'
    break
  case 2:
    Ref = 'bankTableRef'
    break
  case 3:
    Ref = 'concatTableRef'
    break
  }

  this.$refs[Ref].$refs.elTable.insert().then(({ row }) => {
    if (activeField) {
      this.$refs[Ref].$refs.elTable
        .setActiveCell(row, activeField)
    } else {
      this.$refs[Ref].$refs.elTable
        .setActiveRow(row)
    }
  })
}

后面在合同模板单据里面也有这种几个表格的分录,就优化成了这样

10.gif

const tableOptions = { // 表格配置项
  initTable: {
    ref: 'initTableRef',
    selectionList: 'sourceFileEntrys',
    linkage: 'templateType', // 是否是联动表格(必选有某值[templateType]才能出现)
  },
  templateTable: {
    ref: 'templateTableRef',
    selectionList: 'tempTemplateList',
    linkage: 'templateType', // 是否是联动表格(必选有某值[templateType]才能出现)
  },
  essayTable: {
    ref: 'essayTableRef',
    selectionList: 'templateSettings',
  },
}

/**
  * @description: 增加行
  * @param {String} type:initTable -> 原始附件 templateTable -> 模板附件  essayTable -> 合同范文模板配置
  * @return {*}
  * @Date Changed:
*/
insertRow(type) {
  const tableObj = tableOptions[type]
  if (!tableObj) return

  const {
    ref,
    linkage
  } = tableObj
  const templateType = this.formModel[linkage]
  if (linkage && !templateType) {
    return this.$message.error('请选择模板类型!')
  }
}

写在最后

footer.png

如果文章中有那块写的不太好或有问题欢迎大家指出,我也会在后面的文章不停修改。也希望自己进步的同时能跟你们一起成长。喜欢我文章的朋友们也可以关注一下

我会很感激第一批关注我的人。此时,年轻的我和你,轻装上阵;而后,富裕的你和我,满载而归。

往期文章

【建议追更】以模块化的思想来搭建中后台项目

【以模块化的思想开发中后台项目】第一章

【前端体系】从一道面试题谈谈对EventLoop的理解(更新了四道进阶题的解析)

【前端体系】从地基开始打造一座万丈高楼

【前端体系】正则在开发中的应用场景可不只是规则校验

「函数式编程的实用场景 | 掘金技术征文-双节特别篇」

【建议收藏】css晦涩难懂的点都在这啦