不管是平时在学习js中还是在项目书中写js代码,都避免不了一个问题就是有时候要做大量的分支判断,很多人的第一反应就是使用if else
。无可厚非,if else
早平时做分支判断的时候是非常好用的,但是代码中嵌套的if/else
结构往往导致代码不美观,也不易于理解而且性能低下。所以有时候在我们做项目的时候不可避免的一点的就是要做一些代码的性能以及逻辑的优化。
1简单的逻辑判断常用的优化方法
1.1 使用 ||
if(a){
a = 1;
}else{
a = 0;
};
//可写成
a = a || 0;
1.2 使用三元表达式
var a = 1;
var b = 2;
var c = 3;
var d = 4;
if(a == b){
a = c;
}else{
a = d;
}
//可写成
a = (a == b) ? c : d;
1.3 按位异或运算符^
var a = 1;
var b = 2;
var c = 1;
if(a == c){
c = b;
}else if(b == c){
c = a;
};
//可写成
c = a ^ b ^ c;
二、复杂的逻辑判断常用的优化方法
2.1 优化if逻辑
人们考虑的东西到时候,都会把最可能发生的情况先做好准备。优化if逻辑的时候也可以这样想:把最可能出现的条件放在前面,把最不可能出现的条件放在后面,这样程序执行时总会按照写的逻辑的先后顺序逐一检测所有的条件,知道发现匹配的条件才会停止继续检测。
if的优化目标:最小化找到分支之前所判断条件体的数量。 if优化的方法:将最常见的条件放在首位。
var a = 1;
if(a < 10){
//代码
}else if(a > 10 && a < 100){
//代码
}else{
//代码
}
以上只有在a值经常出现小于5的时候是最优化的。如果a值经常大于或者等于10的话,那么在进入正确的分支之前,就必须两次运算条件体,导致表达式的平均运算时间增加。if中的条件体应该总是按照从最大概率到最小概率排列,以保证理论速度最快。
2.2 switch/case
switch和if else在性能上是没有什么区别的,主要还是根据需求进行分析和选择
条件较小的话选用if else比较合适。 条件数量较大的话,就建议选用switch。 在大多数的情况下switch比if else运行的更加快。
var title = document.querySelector('h1'); //h1节点对象
var txt = title.innerText; //h1节点文本内容
var dayText = ''; //星期几 对应的文本
var date = new Date(); //日期对象
var day = date.getDay(); //获得 星期几 0 - 6
switch (day) {
case 0:
dayText = '日';
break;
case 1:
dayText = '一';
break;
case 2:
dayText = '二';
break;
case 3:
dayText = '三';
break;
case 4:
dayText = '四';
break;
case 5:
dayText = '五';
break;
case 6:
dayText = '六';
break;
default:
break;
}
title.innerText = txt.substr(0,5)+ dayText;
上述的逻辑情况在具体的判断选择上,不管是代码的优雅程度还是性能上明显switch是比要if else要优。
2.3 数组映射
在数据查找速度方面,如果能够直接映射到的查找方式绝对比if else判断包括switch的性能好的太多。在js中,熟练的应用数组(包括后面提到的JSON),不管是在数据的存储方面还是在业务逻辑的优化方面绝对是所有做前端开发者中必须套掌握的。
//用空间换取时间
var dayArr = ['天','一','二','三','四','五','六'];
//用day做下标 指引元素
dayText = dayArr[day];
title.innerText = txt.substr(0,5)+ dayText;
上述代码就是通过映射的方式来查找数据,直接省去了诸多的判断过程。
2.4 使用JSON 优化
在前后台传输数据的过程中,现在用的越来越多的传输的数据格式为JSON,第一是因为JSON是基于文本的数据格式,相对于基于二进制的数据,所以JSON在传递的时候是传递符合JSON这种格式的字符串;第二就是JSON比较轻量,即相同数据,以JSON的格式占据的带宽更小,这在有大量数据请求和传递的情况下是有明显优势的。
var data = {
"0" : "日",
"1" : "一",
"2" : "二",
"3" : "三",
"4" : "四",
"5" : "五",
"6" : "六",
};
//用key做下标 指引元素
dayText = data[key];
title.innerText = txt.substr(0,5)+ dayText;
2.5 重构,用 OO 里面的继承或者组合
如果是乔丹,就是23
如果是科比,就是24
如果是韦德,就是3
如果是麦迪,就是1
如果都不是,就是0
来重构一下,改成OO
*定义类: 球员(或者接口)
*定义方法:就是
*定义子类:乔丹、科比、韦德、麦迪、无
*重写方法 ---- 就是
定义一个函数,如果说是用if else:
function getNumber(name) {
if (name === "乔丹") {
console.log(23);
} else if (name === "科比") {
console.log(24);
} else if (name === "韦德"){
console.log(3);
}else if (name === "麦迪"){
console.log(1);
}else{
console.log(0);
}
}
那如果用下面的方法会更好:
function getNumber(name){
var player = {
"乔丹" : "23",
"科比" : "24",
"韦德" : "3",
"麦迪" : "1",
"无" : "0"
};
console.log(player[name] ? player[name] : player["无"] );
}
场景案例
场景一: 根据status显示对应名称
优化方案1:object对象
const statusStr = {
'1': '待付款',
'2': '待发货',
'3': '已发货',
'4': '交易完成',
'5': '交易关闭',
'default': '',
}
const getStatus = (status) =>{
return statusStr[status] || statusStr['default']
}
将判断条件作为对象的属性名,将处理逻辑作为对象的属性值,在按钮点击的时候,通过对象属性查找的方式来进行逻辑判断.
优化方案2: Map对象
const statusStr = new map([
'1': ['待付款'],
'2': ['待发货'],
'3': ['已发货'],
'4': ['交易完成'],
'5': ['交易关闭'],
'default': [''],
])
const getStatus = (status) =>{
let actions = statusStr.get(status) || statusStr.get('default')
return actions[0];
}
这样写用到了es6里的Map对象,那么Map对象和Object对象有什么区别呢?
一个对象通常都有自己的原型,所以一个对象总有一个"prototype"键。 一个对象的键只能是字符串或者Symbols,但一个Map的键可以是任意值。 你可以通过size属性很容易地得到一个Map的键值对个数,而对象的键值对个数只能手动确认。
场景二:多个condition对应名称
现在把问题升级一下, 以前按钮点击时候只需要判断status,现在还需要判断用户的身份:
「举个栗子:」
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
}
}
}
上面的例子我们可以看到,当你的逻辑升级为二元判断时,你的判断量会加倍,你的代码量也会加倍,这时怎么写更清爽呢?
优化方案1: 将condition用字符拼接形式存在Map对象里
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对象进行查找并执行,这种写法在多元条件判断时候尤其好用。
优化方案2: 将condition用字符拼接形式存在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)
}
优化方案3: 将condition用Object对象形式存在Map对象里
可能用查询条件拼成字符串有点别扭,那还有一种方案,就是用Map对象,以Object对象作为key:
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))
}
场景三:根据status做出相应操作
「举个栗子:」
function init () {
if (isAnswer === 1) {
if (isOldUser === 1) {
// ...
} else if (isOldUser === 2) {
// ...
}
} else if (isAnswer === 2) {
if (isOldUser === 1) {
// ...
} else if (isOldUser === 2) {
// ...
}
} else if (isAnswer === 3) {
if (isOldUser === 1) {
// ...
} else if (isOldUser === 2) {
// ...
}
}
}
优化方案1: 查找表,职责链查找表
const rules = [
{
match (an, old) {if (an === 1) {return true}},
action (an, old) {
if (old === 1) {// ...}
else if (old === 2) {// ...}
}
},
{
match (an, old) { if (an === 2) {return true } },
action (an, old) {
if (old === 1) {// ...}
else if (old === 2) {// ...}
}
},
{
match (an, old) {if (an === 3) {return true}},
action (an, old) {
if (old === 1) {// ...}
else if (old === 2) {// ...}
}
}
]
function init (an, old) {
for (let i = 0; i < rules.length; i++) {
// 如果返回true
if (rules[i].match(an, old)) {
rules[i].action(an, old)
}
}
}
init(isAnswer, isOldUser)
虽然可能看着是治标不治本,其实不然,init函数的复杂度大大的降低了。我们已经把控制流程的复杂逻辑,拆分到determineAction函数中
优化方案2: 函数式编程
import R from 'ramda'
var fn = R.cond([
[R.equals(0), R.always('water freezes at 0°C')],
[R.equals(100), R.always('water boils at 100°C')],
[R.T, temp => 'nothing special happens at ' + temp + '°C']
]);
fn(0); //=> 'water freezes at 0°C'
fn(50); //=> 'nothing special happens at 50°C'
fn(100); //=> 'water boils at 100°C'
场景四: 根据范围去进行不同处理
「举个栗子:」比如大家可能会遇到类似下面的需求:比如某平台的信用分数评级,超过700-950,就是信用极好,650-700信用优秀,600-650信用良好,550-600信用中等,350-550信用较差。
function showGrace(grace) {
let _level='';
if(grace>=700){
_level='信用极好'
}
else if(grace>=650){
_level='信用优秀'
}
else if(grace>=600){
_level='信用良好'
}
else if(grace>=550){
_level='信用中等'
}
else{
_level='信用较差'
}
return _level;
}
优化方案1: 用look-up表,把配置数据和业务逻辑分离
function showGrace(grace,level,levelForGrace) {
for(let i=0;i<level.length;i++){
if(grace>=level[i]){
return levelForGrace[i];
}
}
//如果不存在,那么就是分数很低,返回最后一个
return levelForGrace[levelForGrace.length-1];
}
let graceForLevel=[700,650,600,550];
let levelText=['信用极好','信用优秀','信用良好','信用中等','信用较差'];
网友评论