我们在写JavaScript代码的时候,经常会遇到逻辑判断比较复杂的情况,通常我们可以使用if/else或者switch来实现多个条件判断。
但是有一个问题,随着逻辑复杂度的增加,代码中的if/else/switch会越来越臃肿,难以理解。那么如何才能写出更优雅的判断逻辑呢?
示例:
/**
* Button click event
* @param {number} status:1 2 3 4 5
*/
const onButtonClick = (status)=>{
if(status == 1){
jumpTo('IndexPage')
}else if(status == 2){
jumpTo('FailPage')
}else if(status == 3){
jumpTo('FailPage')
}else if(status == 4){
jumpTo('SuccessPage')
}else if(status == 5){
jumpTo('CancelPage')
}else {
jumpTo('Index')
}
}
从代码中我们可以看到这个按钮的点击逻辑:根据不同的活动状态,做两件事,发送日志事件,跳转到对应页面。大家很容易就提出使用 switch 进行代码重写方案:
const onButtonClick = (status)=>{
switch (status){
case 1:
console.log('IndexPage')
break
case 2:
case 3:
jumpTo('FailPage')
break
case 4:
jumpTo('SuccessPage')
break
case 5:
jumpTo('CancelPage')
break
default:
jumpTo('Index')
break
}
}
这样看起来比用 if/else 清晰多了,你还发现了一个小技巧:当 case 2 和 case 3 的逻辑相同时,可以省略执行语句和 break,这样 case 2 的逻辑就会自动执行 case 3 的逻辑。
持续优化:
const actions = {
'1': ['IndexPage'],
'2': ['FailPage'],
'3': ['FailPage'],
'4': ['SuccessPage'],
'5': ['CancelPage'],
'default': ['Index'],
}
const onButtonClick = (status)=>{
let action = actions[status] || actions['default'],
jumpTo(action[0])
}
现在代码确实看起来干净多了,这个方法的巧妙之处在于:把判定条件作为对象的属性名,把处理逻辑作为对象的属性值,点击按钮时通过查找对象属性进行逻辑判断,这种写法特别适合一元条件判断。
还有其他写法吗?用map:
const actions = new Map([
[1, ['IndexPage']],
[2, ['FailPage']],
[3, ['FailPage']],
[4, ['SuccessPage']],
[5, ['CancelPage']],
['default', ['Index']]
])
const onButtonClick = (status)=>{
let action = actions.get(status) || actions.get('default')
jumpTo(action[0])
}
这样写的话,就用到了ES6中的Map对象,是不是感觉顺畅多了?Map对象和Object有什么区别?
- 对象通常有自己的原型,所以对象总有一个原型键。
- 对象的键只能是字符串或者Symbol,而Map的键可以是任意值。
Map中键值对的数量可以通过size属性轻松获取,而对象中键值对的数量只能手动确认。
复杂一点的话,再加一层判断如何?
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 = 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*/}],
])
const onButtonClick = (identity,status)=>{
let action = actions.get(`${identity}_${status}`) || actions.get('default')
action.call(this)
}
上述代码的核心逻辑是:将两个条件拼接成一个字符串,以拼接后的条件字符串为键,以处理函数为值,通过 Map 对象查找并执行。这种方法在做多条件判断的时候特别有用。
当然,使用 Object 实现上述代码也是类似的:
const actions = {
'guest_1':()=>{/*do sth*/},
'guest_2':()=>{/*do sth*/},
//....
}
const onButtonClick = (identity,status)=>{
let action = actions[`${identity}_${status}`] || actions['default']
action.call(this)
}
如果把查询条件拼接成字符串感觉有点别扭的话,还有另外一个解决办法,就是使用一个以 Object 对象为键的 Map 对象:
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。
我们再增加一点难度,假设一个客人的情况,状态1-4的处理逻辑都一样,我们该如何处理呢?最坏的情况是这样的:
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))
}
这样写已经可以满足日常需求了。
不过说真的,重写 functionA 四次还是有点麻烦的。如果情况变得特别复杂,比如,身份有三种状态,状态有十种状态,那么就需要定义 30 个处理逻辑。
而且往往这些逻辑很多都是相同的,这看起来有点让人难以接受。那么可以这样实现:
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 的优势就更加凸显了,它允许使用正则类型作为键,这带来了无限可能。假设需求发生了变化,每个客人场景都需要发送日志事件,不同的状态场景也需要单独的逻辑处理。那么,我们可以这样写:
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))
}
也就是说,利用数组循环的特性,满足正则表达式条件的逻辑就会被执行,这样就可以同时执行通用逻辑和个别逻辑。有了正则表达式的存在,你可以发挥你的想象力,解锁更多的可能性。