chiwent / blog

个人博客,只在issue内更新
https://chiwent.github.io/blog
8 stars 0 forks source link

优化JavaScript代码的细节 #15

Open chiwent opened 5 years ago

chiwent commented 5 years ago

优化JavaScript代码的细节

1. 关于条件语句

1.处理if语句中对单个变量值的多次逻辑运算

通常,我们在一个if判断语句中可能会对单个变量进行多次的比较,然后再取逻辑或,比如下面

function test(param) {
    if (param === 1 || param === 2 || param === 3) {
        // ...
    }
}

这样,if语句不仅会变得累赘,而且一旦增加判断条件,那么就要在if语句中继续加入条件,不利于扩展。我们可以将判定的阈值放在数组中,然后通过Array.includes去查询是否有符合数组内元素的条件:

function test(param) {
    let arr = [1, 2, 3];
    if (arr.includes(param)) {
        // ...
    }
}

或者也可以使用Array.some实现,不过在这个情景中还是推荐使用includes:

function test(param) {
    let arr = [1, 2, 3];
    arr.some(item => {
        if (item === param) {
            // ...
        }
    });
}

2.用return提前返回,减少if嵌套

在开发中,我们可能会遇到这个代码场景,需要在第一层if中对某个值进行判断其是否符合条件,然后在该if判断语句中再进行其他的逻辑判断,这样就增加了if的嵌套层数:

function test(arr, param) {
    if (arr.length > 0) {
        if (param === 1) {
            // ...
        }
    }
}

在上面的代码中,只有当传入的参数arr不为空数组时,才会进入到主体的操作。
其实我们完全将第一层if判断抽离到顶部,在判断不符合条件就马上return:

function test(arr, param) {
    if (arr.length === 0) {
        return;
    }
    if (param === 1) {
        // ...
    }
}

所以,在遇到类似上述的if嵌套判断时,我们可以在发现无效条件时提前return

3.将默认值提升到默认参数中

举个在vue代码中常见的栗子,如果我们要对data的默认值和传入的参数进行逻辑或,可能会有下面的代码:

export default {
    data() {
        return {
            condition: 1
        }
    },
    methods: {
        test(param) {
            const _data = param || this.condition;
            this.excute(_data);
        },
        excute(param) {
            // ...
        }
    }
}

在上述的test方法中,如果传入的param不为空,我们就在excute中传入param;如果param为空,就使用默认的this.condition值。实际上,如果我们将默认值提前,就可以减少_data这个临时变量:

export default {
    data() {
        return {
            condition: 1
        }
    },
    methods: {
        test(param = this.confition) {
            // const _data = param || this.condition; // 减少了这一句
            this.excute(_data);
        },
        excute(param) {
            // ...
        }
    }
}

4.使用every/some处理全部/部分满足条件

没有优化过的代码如下:

const fruits = [
    { name: 'apple', color: 'red' },
    { name: 'banana', color: 'yellow' },
    { name: 'grape', color: 'purple' }
  ];

function test() {
  let isAllRed = true;
  // 所有的水果都必须是红色
  for (let f of fruits) {
    if (!isAllRed) break;
    isAllRed = (f.color === 'red');
  }
  console.log(isAllRed); // false
}

上述代码的意图就是为了判断所有的水果是否全为红色。

在处理全部/部分满足的条件时,我们通常可以使用every/some来进行处理:

const fruits = [
    { name: 'apple', color: 'red' },
    { name: 'banana', color: 'yellow' },
    { name: 'grape', color: 'purple' }
  ];
function test() {
  // 简短方式,所有的水果都必须是红色
  const isAllRed = fruits.every(f => f.color == 'red');
  console.log(isAllRed); // false
}

同理,如果是要检查是否至少有一类水果是红色的,就将every改写为some。

5.删除冗余的if...else if...if语句

在开发中,对单个变量的多个条件进行判断,就可能产生if判断的面条代码。在这种情况下,我们可以使用switch...case进行初步优化:

function test(color) {
  // 使用 switch case 语句,根据颜色找出对应的水果
  switch (color) {
    case 'red':
      return ['apple', 'strawberry'];
    case 'yellow':
      return ['banana', 'pineapple'];
    case 'purple':
      return ['grape', 'plum'];
    default:
      return [];
  }
}

不过switch...case依旧不是最优的处理方式。假如在判断条件内没有进行太复杂的运算,我们可以考虑使用高阶函数、Map对象和Object字面量来处理,比如:

// 方法1:Object字面量
const fruitColor = {
    red: ['apple', 'strawberry'],
    yellow: ['banana', 'pineapple'],
    purple: ['grape', 'plum']
};

function test(color) {
  return fruitColor[color] || [];
}

// 方法2:Map对象
const fruitColor = new Map()
    .set('red', ['apple', 'strawberry'])
    .set('yellow', ['banana', 'pineapple'])
    .set('purple', ['grape', 'plum']);

function test(color) {
  return fruitColor.get(color) || [];
}

// 方法3:高阶函数
const fruits = [
    { name: 'apple', color: 'red' }, 
    { name: 'strawberry', color: 'red' }, 
    { name: 'banana', color: 'yellow' }, 
    { name: 'pineapple', color: 'yellow' }, 
    { name: 'grape', color: 'purple' }, 
    { name: 'plum', color: 'purple' }
];
function test(color) {
  return fruits.filter(f => f.color == color);
}

除了上述的方式,我们也可以使用策略模式进行优化,常见的场景就是表单验证,由于需要对表单值进行多条件的判断,可能会产生较长的if...else...if语句,可以使用策略模式优化,详情见:探索两种优雅的表单验证——策略设计模式和ES6的Proxy代理模式

使用策略模式来优化表单校验:

/*策略对象*/
const strategies = {
    isNonEmpty(value, errorMsg) {
        return value === '' ?
            errorMsg : void 0
    },
    minLength(value, length, errorMsg) {
        return value.length < length ?
            errorMsg : void 0
    },
    isMoblie(value, errorMsg) {
        return !/^1(3|5|7|8|9)[0-9]{9}$/.test(value) ?
            errorMsg : void 0
    },
    isEmail(value, errorMsg) {
        return !/^\w+([+-.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(value) ?
            errorMsg : void 0
    }
}
class Validator {
    constructor() {
        this.ruleList = [];
    }
    add(dom, rules) {
        for (let rule of rules) {
            // 策略规则,如'minLength:6',会拆分为['minLength', '6'];
            const strategyAry = rule.strategy.split(':');
            // 错误信息
            const errMsg = rule.errMsg;
            this.ruleList.push(() => {
                // 策略规则
                const strategy = strategyAry.shift();
                strategyAry.unshift(dom.value);
                strategyAry.push(errMsg);
                return strategies[strategy].apply(dom, strategyAry);
            });
        }
    }
    start() {
        for (let validatorFunc of this.ruleList) {
            const errMsg = validatorFunc();
            if (errMsg) {
                return errMsg;
            }
        }
        return false;
    }
}

// 使用
const form = document.getElementById('#form');
const validatorFunc = () => {
    const validator = new Validator();
    validator.add(form.userName, [{
        strategy: 'isNonEmpty',
        errMsg: '用户名不能为空',
    }, {
        strategy: 'minLength:6',
        errMsg: '用户名长度不能小于六位'
    }]);
    validator.add(form.password, [{
        strategy: 'isNonEmpty',
        errMsg: '密码不能为空',
    }, {
        strategy: 'minLength:6',
        errMsg: '密码长度不能小于六位'
    }]);
    const errMsg = validatorFunc();
    return errMsg;
}

form.addEventListener('submit', () => {
    const errMsg = validatorFunc();
    if (errMsg) {
        alert(errMsg);
    }
});

2.关于出入参

1.消除大量的入参

在调用函数时,可能需要对函数传入大量的参数,日积月累,函数的入参可能会随着功能的迭代不断增加,所以我们需要将这些参数放入对象中:

// bad
function test(a, b, c, d, e) {
    console.log(a)
    console.log(b)
    console.log(c)
    console.log(d)
    console.log(e)
}
test(1, 2, 3, 4, 5);

// good
function test(obj) {
    console.log(obj.a)
    console.log(obj.b)
    console.log(obj.c)
    console.log(obj.d)
    console.log(obj.e)
}
test({
    a: 1,
    b: 2,
    c: 3,
    d: 4,
    e: 5
})

对象操作

多嵌套对象属性的判断

在实际开发中,我们经常会对一个多层嵌套的对象进行判断,比如:if(obj.a.b.c),但是假如obj这个对象为空,或者其中的某个属性为空,那么代码就会报错。之后,我们可能就会写出这样的代码:if (obj && obj.a && obj.a.b && obj.a.b.c),这样的代码就很冗余,而且嵌套的层级越多,代码越啰嗦,那么有什么方法可以改善这种情况呢?

// 直接使用 const property = judgeObjProperty(); if (property) { // ... }

// 稍微封装一下 function judgeObjProperty(property) { try { return property; } catch (err) { return undefined; } }

// 使用: const property = judgeObjProperty(obj.a.b.c); if (property) { // ... }

// 或者: function judgeObjProperty(data, expression) { try { return eval('data' + expression) } catch (error) { return undefined } }

// 使用: judgeObjProperty(obj, '.a.b.c');


- 2.遍历取值
```js
function judgeObjProperty(data, keys) {
  for(let key of keys) {
    if(data[key] === undefined) {
      return undefined
    }
    else {
      data = data[key]
    }
  }
  return data
}
judgeObjProperty(bj, ['a', 'b', 'c'])




未完待续......