前提
我们编写js代码时经常遇到复杂逻辑判断的情况,通常大家可以用if/else或者switch来实现多个条件判断,但这样会有个问题,随着逻辑复杂度的增加,代码中的if/else/switch会变得越来越臃肿,越来越看不懂,那么如何更优雅的写判断逻辑,本文带你试一下。
举个例子
先看一段代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /** * 按钮点击事件 * @param {number} status 活动状态:1 开团进行中 2 开团失败 3 商品售罄 4 开团成功 5 系统取消 */ const onButtonClick = (status)=>{ if (status == 1){ sendLog( 'processing' ) jumpTo( 'IndexPage' ) } else if (status == 2){ sendLog( 'fail' ) jumpTo( 'FailPage' ) } else if (status == 3){ sendLog( 'fail' ) jumpTo( 'FailPage' ) } else if (status == 4){ sendLog( 'success' ) jumpTo( 'SuccessPage' ) } else if (status == 5){ sendLog( 'cancel' ) jumpTo( 'CancelPage' ) } else { sendLog( 'other' ) jumpTo( 'Index' ) } } |
通过代码可以看到这个按钮的点击逻辑:根据不同活动状态做两件事情,发送日志埋点和跳转到对应页面,大家可以很轻易的提出这段代码的改写方案,switch出场:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | /** * 按钮点击事件 * @param {number} status 活动状态:1 开团进行中 2 开团失败 3 商品售罄 4 开团成功 5 系统取消 */ const onButtonClick = (status)=>{ switch (status){ case 1: sendLog( 'processing' ) jumpTo( 'IndexPage' ) break case 2: case 3: sendLog( 'fail' ) jumpTo( 'FailPage' ) break case 4: sendLog( 'success' ) jumpTo( 'SuccessPage' ) break case 5: sendLog( 'cancel' ) jumpTo( 'CancelPage' ) break default : sendLog( 'other' ) jumpTo( 'Index' ) break } } |
嗯,这样看起来比if/else清晰多了,细心的同学也发现了小技巧,case 2和case 3逻辑一样的时候,可以省去执行语句和break,则case 2的情况自动执行case 3的逻辑。
这时有同学会说,还有更简单的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | const actions = { '1' : [ 'processing' , 'IndexPage' ], '2' : [ 'fail' , 'FailPage' ], '3' : [ 'fail' , 'FailPage' ], '4' : [ 'success' , 'SuccessPage' ], '5' : [ 'cancel' , 'CancelPage' ], 'default' : [ 'other' , 'Index' ], } /** * 按钮点击事件 * @param {number} status 活动状态:1开团进行中 2开团失败 3 商品售罄 4 开团成功 5 系统取消 */ const onButtonClick = (status)=>{ let action = actions[status] || actions[ 'default' ], logName = action[0], pageName = action[1] sendLog(logName) jumpTo(pageName) } |
上面代码确实看起来更清爽了,这种方法的聪明之处在于:将判断条件作为对象的属性名,将处理逻辑作为对象的属性值,在按钮点击的时候,通过对象属性查找的方式来进行逻辑判断,这种写法特别适合一元条件判断的情况。
是不是还有其他写法呢?有的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | const actions = new Map([ [1, [ 'processing' , 'IndexPage' ]], [2, [ 'fail' , 'FailPage' ]], [3, [ 'fail' , 'FailPage' ]], [4, [ 'success' , 'SuccessPage' ]], [5, [ 'cancel' , 'CancelPage' ]], [ 'default' , [ 'other' , 'Index' ]] ]) /** * 按钮点击事件 * @param {number} status 活动状态:1 开团进行中 2 开团失败 3 商品售罄 4 开团成功 5 系统取消 */ const onButtonClick = (status)=>{ let action = actions.get(status) || actions.get( 'default' ) sendLog(action[0]) jumpTo(action[1]) } |
这样写用到了es6里的Map对象,是不是更爽了?Map对象和Object对象有什么区别呢?
1、一个对象通常都有自己的原型,所以一个对象总有一个"prototype"键。
2、一个对象的键只能是字符串或者Symbols,但一个Map的键可以是任意值。
3、你可以通过size属性很容易地得到一个Map的键值对个数,而对象的键值对个数只能手动确认。
我们需要把问题升级一下,以前按钮点击时候只需要判断status,现在还需要判断用户的身份:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | /** * 按钮点击事件 * @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 } } } |
原谅我不写每个判断里的具体逻辑了,因为代码太冗长了。
原谅我又用了if/else,因为我看到很多人依然在用if/else写这种大段的逻辑判断。
从上面的例子我们可以看到,当你的逻辑升级为二元判断时,你的判断量会加倍,你的代码量也会加倍,这时怎么写更清爽呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | const actions = new Map([ [ 'guest_1' , ()=>{ /*do sth*/ }], [ 'guest_2' , ()=>{ /*do sth*/ }], [ 'guest_3' , ()=>{ /*do sth*/ }], [ 'guest_4' , ()=>{ /*do sth*/ }], [ 'guest_5' , ()=>{ /*do sth*/ }], [ 'master_1' , ()=>{ /*do sth*/ }], [ 'master_2' , ()=>{ /*do sth*/ }], [ 'master_3' , ()=>{ /*do sth*/ }], [ 'master_4' , ()=>{ /*do sth*/ }], [ 'master_5' , ()=>{ /*do sth*/ }], [ 'default' , ()=>{ /*do sth*/ }], ]) /** * 按钮点击事件 * @param {string} identity 身份标识:guest客态 master主态 * @param {number} status 活动状态:1 开团进行中 2 开团失败 3 开团成功 4 商品售罄 5 有库存未开团 */ const onButtonClick = (identity,status)=>{ let action = actions.get(`${identity}_${status}`) || actions.get( 'default' ) action.call( this ) } |
上述代码核心逻辑是:把两个条件拼接成字符串,并通过以条件拼接字符串作为键,以处理函数作为值的Map对象进行查找并执行,这种写法在多元条件判断时候尤其好用。
当然上述代码如果用Object对象来实现也是类似的:
1 2 3 4 5 6 7 8 9 10 | const actions = { 'guest_1' :()=>{ /*do sth*/ }, 'guest_2' :()=>{ /*do sth*/ }, //.... } const onButtonClick = (identity,status)=>{ let action = actions[`${identity}_${status}`] || actions[ 'default' ] action.call( this ) } |
如果有些同学觉得把查询条件拼成字符串有点别扭,那还有一种方案,就是用Map对象,以Object对象作为key:
1 2 3 4 5 6 7 8 9 10 | const actions = new Map([ [{identity: 'guest' ,status:1},()=>{ /*do sth*/ }], [{identity: 'guest' ,status:2},()=>{ /*do sth*/ }], //... ]) const onButtonClick = (identity,status)=>{ let action = [...actions].filter(([key,value])=>(key.identity == identity && key.status == status)) action.forEach(([key,value])=>value.call( this )) } |
是不是又高级了一点点?
这里也看出来Map与Object的区别,Map可以用任何类型的数据作为key。
我们现在再将难度升级一点点,假如guest情况下,status1-4的处理逻辑都一样怎么办,最差的情况是这样:
1 2 3 4 5 6 7 8 | const actions = new Map([ [{identity: 'guest' ,status:1},()=>{ /* functionA */ }], [{identity: 'guest' ,status:2},()=>{ /* functionA */ }], [{identity: 'guest' ,status:3},()=>{ /* functionA */ }], [{identity: 'guest' ,status:4},()=>{ /* functionA */ }], [{identity: 'guest' ,status:5},()=>{ /* functionB */ }], //... ]) |
好一点的写法是将处理逻辑函数进行缓存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | const actions = ()=>{ const functionA = ()=>{ /*do sth*/ } const functionB = ()=>{ /*do sth*/ } return new Map([ [{identity: 'guest' ,status:1},functionA], [{identity: 'guest' ,status:2},functionA], [{identity: 'guest' ,status:3},functionA], [{identity: 'guest' ,status:4},functionA], [{identity: 'guest' ,status:5},functionB], //... ]) } const onButtonClick = (identity,status)=>{ let action = [...actions()].filter(([key,value])=>(key.identity == identity && key.status == status)) action.forEach(([key,value])=>value.call( this )) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | const actions = ()=>{ const functionA = ()=>{ /*do sth*/ } const functionB = ()=>{ /*do sth*/ } return new Map([ [/^guest_[1-4]$/,functionA], [/^guest_5$/,functionB], //... ]) } const onButtonClick = (identity,status)=>{ let action = [...actions()].filter(([key,value])=>(key.test(`${identity}_${status}`))) action.forEach(([key,value])=>value.call( this )) } |
这里Map的优势更加凸显,可以用正则类型作为key了,这样就有了无限可能,假如需求变成,凡是guest情况都要发送一个日志埋点,不同status情况也需要单独的逻辑处理,那我们可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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 )) } |
也就是说利用数组循环的特性,符合正则条件的逻辑都会被执行,那就可以同时执行公共逻辑和单独逻辑,因为正则的存在,你可以打开想象力解锁更多的玩法,本文就不赘述了。
总结
本文已经教你了8种逻辑判断写法,包括:
1、if/else
2、switch
3、一元判断时:存到Object里
4、一元判断时:存到Map里
5、多元判断时:将condition拼接成字符串存到Object里
6、多元判断时:将condition拼接成字符串存到Map里
7、多元判断时:将condition存为Object存到Map里
8、多元判断时:将condition写作正则存到Map里
至此,本文也将告一段落,愿你未来的人生里,不只是有if/else/switch。
在卷的地方,测试要比开发还要开发,又要懂业务又要懂测试,还要懂运维,我都搞不懂现在测试到底是个什么角色了
懂了懂了
学习到了 感谢分享
请问 一下,我本科就是软件工程(软件测试方向),以后也想成为软件测试工程师,目前大三即将结束,我之前是准备考研 ,也只是知道考研没有考虑具体什么方向之类的。因为软件测试是专业课 大三下才开课,我现在发现考研的学校 基本没有 软件测试方向的,都是比较热门的大数据、人工智能等研究方向。 所以 想成为软件测试工程师 是在大四时好好学习技术 然后本来毕业找工作?还是 应该考研究生(只是 我发现研究生没有研究软件测试的,也可能我没关注到) ?