【转】JS条件判断小技巧
经常code review,我发现很容易写出一堆冗长的代码。今天就列几个比较常见的“解决之道”,看看如何减少JS里的条件判断。
提前返回,少用if...else
但是过多的嵌套,还是挺令人抓狂的。这里有一个很典型的条件嵌套:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function func() { var result; if (conditionA) { if (condintionB) { result = 'Success' ; } else { result = 'Error1' ; } } else { result = 'Error2' ; } return result; } |
这种嵌套的特点就是else里的代码块很小,但是由于不得不做的分支语句导致多层嵌套。动动脑筋,怎样精简一下呢?在if里做非判断——条件反转,并通过卫语句提前return else分支。
1
2
3
4
5
6
7
8
9
10
|
function func() { if (!conditionA) { return 'Error2' ; } if (!condintionB) { return 'Error1' ; } return 'Success' ; } |
forEach优化
遍历的时候也经常产生大量的嵌套,如下代码所示,我们先对数组元素做一次合法性校验,通过后再做一次新的操作,最后把操作结果追加到新数组里。
1
2
3
4
5
6
7
8
9
|
const func = (arr) => { const res = []; arr.forEach((e) => { if (e !== 'Onion' ) { res.push(`${e} Run!`); } }) return res; } |
仔细观察这就是一个filter加map的过程。我们不妨试试函数式编程:
1
2
3
4
|
const func = (arr) => { return arr.filer((e) => e !== 'Onion' ) .map((e) => `${e} Run!`); } |
多条件,用Array.includes
再举个例子,某个页面需要检验输入type是否合法。我收到过一个MR曾经是这么写的。
1
2
3
4
5
6
7
|
const init(type) { if (type === 'Seminar' || type === 'Interview' ) { console.log( 'valid' ); } //... console.error( 'invalide' ); } |
如果合法的类型只有两种,代码其实也没啥问题。只是一般的业务很容易有后续的延展。今后将合法类型增加到10种的话,上述代码里将是一大长串的if判断。这种代码可读性极差,我们不如转换一下思想,把非法类型储到数组里,用Array.includes来帮你完成冗长的判断。之后每增加一种新的类型,只需在数组后追加新字符串就行了。
1
2
3
4
5
6
7
8
|
const init(type) { const invalidArray = [ 'Seminar' , 'Interview' ]; if (invalidArray.includes(type)) { console.log( 'valid' ); } //... console.error( 'invalide' ); } |
使用object索引
类似的情况也出现在三元表达式里:
1
2
3
4
|
const dateFormat = (dateTime) => { const format = this .$i18n.locale === 'en' ? 'mmm d, yyyy' : 'yyyy年m月d日' ; return DateFormat(dateTime, format); } |
我们现阶段多语言只有en和zh,上述代码自然不是问题,但是也难保哪天会支持日语——ja。这时候再写成下面这类代码就很搞笑了
1
2
3
|
const format = this .$i18n.locale === 'en' ? 'mmm d, yyyy' : ( this .$i18n.locale === 'zh' ? 'yyyy年m月d日' : 'yyyy/m/d' ); |
比较合适的写法就是使用object键索引,这样当语言业务扩展后,只需要在localeFormats后追加新格式就行了。
1
2
3
4
5
6
|
const localeFormats = { en: 'mmm d, yyyy' , zh: 'yyyy年m月d日' , ja: 'yyyy/m/d' , }; const format = localeFormats[ this .$i18n.locale]; |
尽量少用swith
长Switch也及其难看。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
export function (type) { switch (type) { case 'Onion' : return func1(); case 'Garlic' : return func2(); case 'Ginger' : return func3(); default : return () => { console.error( 'ERROR' ) }; } } |
我记得OOP设计模式里提到过:尽量使用多态和继承代替Switch结构。JS里倒不必非得往这方面想,用Object或是Map索引来代替Switch也是极好滴!
1
2
3
4
5
6
|
const arr = [[ 'Onion' , func1], [ 'Garlic' , func2], [ 'Ginger' , func3],]; const def = () => { console.error( 'ERROR' ) }; const vegetable = new Map(arr); export function (type) { return (vegetable.get(type) || def).call( null ); } |
Optional Chaining
Optional Chaining(以下简称OC)是我极力推荐的一个语法糖。我写过一期《Javascript Optional Chaining》具体介绍它的用法,有兴趣的小伙伴可以看一看,这里稍微点一下。比如我们想获取地址里的街道信息,但是并不确定地址本身是否存在,因此只能在获取街道前,事先判断一下地址合法性,一般我们会这么写:
1
2
3
|
if (address) { var street = address.street; } |
但假如再多一层呢,从basicInfo.address.street这样找下来呢?
1
2
3
4
5
6
|
if (basicInfo) { var address = basicInfo.address; if (address) { var street = address.street; } } |
上面的代码我已经觉得很丑陋了,再多个几层真是没法看了。不过不用担心,有了OC一切迎刃而解。(虽然OC还在ECMAScript stage2,但是大家可以用babel尝鲜;babel会自动把如下源码转义成上面的形式)
1
|
var street = basicInfo?.address?.street; |
OOP 多态
某些业务逻辑本身就十分复杂,嵌套的条件语句在逻辑层面就不可能有所优化了;碰到这类场景,我们又该如何作为呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
function greeting(role, access) { if ( 'owner' === role) { if ( 'public' === access) { //... } if ( 'private' === access) { //... } //... } else if ( 'admin' === role) { if ( 'public' === access) { //... } if ( 'private' === access) { //... } //... } else if ( 'hr' === role) { //... } } |
看一下代码,第一层的if-else判定的是各种角色(role)类别,第二层判定的是角色访问权限设置(access)。这类代码其实并没有特别优雅的处理手段,只能回到《clean code》里最本源的解决手段——把函数写小。本质问题还是函数体过大,而所谓的把大函数拆成多个小函数,事实上就是以抽象换取可读性。
最常规的手段就是 OOP 多态了。上述代码块,第一层的 role 抽象为 User 实例,嵌套层内的各种 access 进一步抽象为 User 的实例方法。
1
2
3
4
5
6
7
8
|
class User { public() { throw new Error( 'Denied!' ); } private () { throw new Error( 'Denied!' ); } } |
Javascript 并没有 interface 这类语法,好在有 class 了,我仿造 interface 写了一个基类如上。接着就是将各种角色抽象为新的子类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
class Owner extends User { public() { console.log( 'Owner in public' ); } private () { console.log( 'Owner inside' ); } } class Admin extends User { public() { console.log( 'Admin in public' ); } private () { console.log( 'Admin inside' ); } } //... |
OOP 推荐使用工厂方法初始化实例,我顺手也写个工厂,这样便可以利用工厂方法消除掉了第一层if-else
1
2
3
4
5
6
7
8
9
|
class UserFactory { static create(role) { if ( 'owner' === role) return new Owner(); else if ( 'admin' === role) return new Admin(); //... } } |
调用的时候我们先通过 role 创建抽象实例,再根据 access 调用具体方法:
1
2
3
4
|
function greeting(role, access) { const user = UserFactory.create(role); user[access](); } |
上面一长串的if-else,一下子被压缩到了两行。这就实现了以抽象(很多可描述的类)换取了可读性(较少的判断嵌套)
调用链
OOP 效果确实很明显,不过上述代码还是过于特例,假如access并不是字符串(如1,2,3),像user[1]这种就很难映射到具体方法了;所以我们往往还要写更细碎的 access 抽象,也便意味着更多的抽象子类,以及新的工厂方法。很多时候,我们也不并需要抽象得尽善尽美。这个场景里写个调用链,也是勉强可用的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
const rules = [ { match(role, access) { return 'owner' === role; }, action(role, access) { if (1 === access) console.log( 'Owner in public' ); else if (2 === access) console.log( 'Owner in private' ); } }, { match(role, access) { return 'admin' === role; }, action(role, access) { //... } } //... ]; |
上面 rules 数组里,每一个元素(rule)里的match被设计用来判定用户权限:遍历数组,若是match为 true,则运行正下方的action——access 相关业务;反之,继续match下一个 rule:
1
2
3
4
|
function greeting(role, access) { rules.find(e => e.match(role)) .action(role, access); } |
最后 greeting 被重构为上述代码。当然,效果没有多态好,只消掉了一层if-else,第二层判定还是留在了 action 里。
AOP
AOP,没看错,Javascript 也是有 AOP 的,只是它的实现要修改 Function 的原型链,不是很推荐;但是Function.prototype.before,Function.prototype.after还是挺常见的,开发组里能协商好,还是可以尝试一下的:
1
2
3
4
5
6
7
8
|
Function.prototype.after = function (next) { let fn = this ; return function $after(...args) { let code = fn.apply( this , args); next.apply( this , args); return code; } } |
传统的 aop after 如上所示。不难看出,用到了高阶函数:具体执行时,先运行函数本体,再运行 after 传进来的 next 方法。为了让 after 应用到我们的话题中,我稍微改一下函数实现:
1
2
3
4
5
6
7
8
9
10
|
const nextSmb = Symbol( 'next' ); Function.prototype.after = function (next) { let fn = this ; return function $after(...args) { let code = fn.apply( this , args); if (nextSmb === code) return next.apply( this , args); return code; } } |
这个 after 实现变成了先运行函数本体,若返回是nextSmb则继续执行后续的 next 方法,反之则停止。有什么用呢?我们看看如何使用:
1
2
3
4
5
6
7
8
9
10
|
function owner(role, access) { function public(access) { return 1 === access ? console.log( 'owner in public' ) : nextSmb; } function private (access) { return 2 === access ? console.log( 'owner in private' ) : nextSmb; } const ownerChain = public.after( private ); return 'owner' === role ? ownerChain(access) : nextSmb; } |
代码还是有点难度的,先看一部分——owner 的定义。这个函数被设计处理role === 'owner'时的逻辑,内部的public和private方法是处理access为 1 和 2 时的逻辑。我们把public和private方法串联成ownerChain(终于用到after方法了),它的作用就是把之前的if-else逻辑抽象成一个上节讲到的函数调用链,在遍历调用链时检查 access 条件:若符合条件,则执行本节点代码,并结束调用链;反之,继续往调用链的后续节点传送。
我把重构后的 greeting 也列一下——单个role的access可以用after串联;不同role之间也可以进一步利用after串起来。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function admin(role, access) { // familiar with owner function public(access) { // } function private (access) { // } const ownerChain = public.after( private ); return 'admin' === role ? ownerChain(access) : nextSmb; } let greeting = owner.after(admin); greeting( 'owner' , 1); |
嗯,这样,我们最原始的greeting方法就被彻底重构了。可以预见,如果调用链很长greeting会是这样:
1
|
let greeting = owner.after(admin).after(hr).after(staff)... |
当然这个方法缺点也很明确,比起之前冗长的代码,可读性增强了,但是理解成本有点高,若团队内没有事先约定,这个维护起来还是挺难的。
作者:anOnion
链接:https://www.jianshu.com/p/6024b1796af5