书写更好的JavaScript条件判断的技巧

原文: https://devinduct.com/blogpost/35/tips-and-tricks-for-better-javascript-conditionals-and-match-criteria
作者:Milos Protic

简介

如果你像我一样喜欢整洁的代码,你可能会想尝试尽可能少的写条件判断语句。一般情况下,面向对象编程可以利用多态和继承来避免使用条件判断。我觉得我们需要尽可能的遵循这些原则。

像我在另一篇文章 JavaScript Clean Code - Best Practices 中提到的那样,你并不是写代码给机器看,而是给“将来的自己”或者是“其他人”看的。

另一方面,出于种种原因,我们可能还是会在代码中使用条件语句。例如可能我们急需处理一个bug,如果不使用条件语句的话就要对原有代码库做很多改变等等。这篇文章可以帮你解决这些问题,并更好的组织使用条件语句。

技巧

以下是一些关于如何构造if...else语句的建议,以及用更少的代码实现更多的功能的技巧。

1. 重要的事情放在第一位,细节也很重要

不要使用否定条件(会让人困惑)并且使用boolean值条件的简写形式。这一点不需要再做强调了,尤其是使用否定条件这一点,不符合正常的思维方式。

不好的写法:

const isEmailNotVerified = (email) => {
  // implementation
}

if (!isEmailNotVerified(email)) {
  // do something...
}

if (isVerified === true) {
  // do something...
}

好的:

const isEmailVerified = (email) => {
  // implementation
}

if (isEmailVerified(email)) {
  // do something…
}

if (isVerified) {
  // do something…
}

好了,现在把这些理清之后我们可以开始了。

2. 多个条件判断使用Array.includes

例如我们想在函数中判断汽车型号是renault还是peugeot,代码可能会这样写:

const checkCarModel = (model) => {
  if(model === 'renault' || model === 'peugeot') { 
    console.log('model valid');
  }
}

checkCarModel('renault'); // outputs 'model valid'

思考一下我们只有两种型号,所以这么写看起来还可以接受,但是如果我们想再增加其他型号的判断呢?或者是两三个或者更多?如果增加更多的or条件,代码就会难以维护并且看起来也不那么整洁。要让代码变得简洁,可以像这样来重写这段代码:

const checkCarModel = (model) => {
  if(['peugeot', 'renault'].includes(model)) { 
    console.log('model valid');
  }
}

checkCarModel('renault'); // outputs 'model valid'

现在看起来好多了,但是如果想要更好一些,我们可以创建一个存储汽车型号的变量:

const checkCarModel = (model) => {
  const models = ['peugeot', 'renault'];

  if(models.includes(model)) { 
    console.log('model valid');
  }
}

checkCarModel('renault'); // outputs 'model valid'

现在,如果我们想增加其他型号的检查,只需要增加新的数组项。并且如果这个变量比较重要,我们可以在这个作用域外的某个地方声明models变量,并且在其他需要的地方复用它。

3. 要匹配所有条件,使用Array.every或者Array.find

在这个例子中,我们想要检查传给函数的车型是否在给定的类型里。要用命令式(imperative)现这个功能,可能会这么写:

const cars = [
  { model: 'renault', year: 1956 },
  { model: 'peugeot', year: 1968 },
  { model: 'ford', year: 1977 }
];

const checkEveryModel = (model) => {
  let isValid = true;

  for (let car of cars) {
    if (!isValid) {
      break;
    }
    isValid = car.model === model;
  }

  return isValid;
}

console.log(checkEveryModel('renault')); // outputs false

如果你想要用命令式的实现方式,上面的代码就还不错。另一方面,如果你并不关心这背后发生了什么,也可以用Array.every或者Array.find重写这个函数来实现同样的功能:

注:命令式编程和声明式编程了解一下

const checkEveryModel = (model) => {
  return cars.every(car => car.model === model);
}

console.log(checkEveryModel('renault')); // outputs false

使用Array.find,做一些小的调整我们可以得到同样的结果,并且性能上是一样的,因为两个函数中都对每个元素使用了回调函数,在发现了错误的值时候直接返回false。

const checkEveryModel = (model) => {
  return cars.find(car => car.model !== model) === undefined;
}

console.log(checkEveryModel('renault')); // outputs false

4. 部分条件匹配使用Array.some

就像Array.every匹配所有条件那样,这个方法可以更简单的实现数组中一个或多个条件的检查。要实现这个效果,需要提供一个对匹配条件返回布尔值的回调函数。

我们可以像之前那样写个小的for...loop声明来实现,幸运的是我们有很酷的JavaScript函数来帮我们做这件事情。

const cars = [
  { model: 'renault', year: 1956 },
  { model: 'peugeot', year: 1968 },
  { model: 'ford', year: 1977 }
];

const checkForAnyModel = (model) => {
  return cars.some(car => car.model === model);
}

console.log(checkForAnyModel('renault')); // outputs true

5. 提前返回而不是使用if...else分支

当我还是个萌新的时候,我被教育道一个函数应该只有一个返回语句,且从一个地方返回。如果小心的处理,这并不是一个坏方法,意味着我们需要考虑可能出现嵌套地狱的情况。如果失去控制,多个分支和if...else嵌套会让人格外痛苦。

另一方面,如果代码库比较大而且有很多行,一个深层的返回语句可能会造成问题。如今我们实践关注点分离和SOLID的原则,可能有很多行数的代码并不多见。

让我们创建个例子来做说明,例子中我们想要展示指定车的型号和生产年份。

const checkModel = (car) => {
  let result; // first, we need to define a result value
  
  // check if car exists
  if(car) {

    // check if car model exists
    if (car.model) {

      // check if car year exists
      if(car.year) {
        result = `Car model: ${car.model}; Manufacturing year: ${car.year};`;
      } else {
        result = 'No car year';
      }
      
    } else {
      result = 'No car model'
    }   

  } else {
    result = 'No car';
  }

  return result; // our single return statement
}

console.log(checkModel()); // outputs 'No car'
console.log(checkModel({ year: 1988 })); // outputs 'No car model'
console.log(checkModel({ model: 'ford' })); // outputs 'No car year'
console.log(checkModel({ model: 'ford', year: 1988 })); // outputs 'Car model: ford; Manufacturing year: 1988;'

可以看见,只是处理简单的问题但代码就特别长。想象一下如果我们要处理更复杂的逻辑时会是怎样。一大坨子if...else语句。

我们可以重构上面的代码,分解更多的步骤来做优化。比如使用三元操作符,增加&&条件等,这里我将直接跳到结果,使用现代的JavaScript特性与多个返回语句,向你展示更简单的实现方式。

const checkModel = ({model, year} = {}) => {
  if(!model && !year) return 'No car';
  if(!model) return 'No car model';
  if(!year) return 'No car year';

    // 这里我们可以随便使用model或year来处理其他逻辑
  // 我们确定他们存在
  // 不需要更多的检查

  // doSomething(model);
  // doSomethingElse(year);
  
  return `Car model: ${model}; Manufacturing year: ${year};`;
}

console.log(checkModel()); // outputs 'No car'
console.log(checkModel({ year: 1988 })); // outputs 'No car model'
console.log(checkModel({ model: 'ford' })); // outputs 'No car year'
console.log(checkModel({ model: 'ford', year: 1988 })); // outputs 'Car model: ford; Manufacturing year: 1988;'

在这个重构过得版本中,我们使用了解构和默认参数。默认参数值可以保证我们在传递undefined的时候也可以解构。注意这里如果我们传递一个null值函数会报错,这也是之前那个写法的优点,因为那种情况下传递null会返回No car

对象解构可以确保函数只获取它需要用到的值,例如我们给car对象中添加属性,在函数中就不会读到它。

开发者们可以依据偏好选择,实践中通常是介于两种方法之间,很多人觉得if...else语句容易理解,能更轻松的遵循程序流程。

6. 使用索引代替switch语句

如果我们想要基于给定国家值获得其拥有的汽车模型

const getCarsByState = (state) => {
  switch (state) {
    case 'usa':
      return ['Ford', 'Dodge'];
    case 'france':
      return ['Renault', 'Peugeot'];
    case 'italy':
      return ['Fiat'];
    default:
      return [];
  }
}

console.log(getCarsByState()); // outputs []
console.log(getCarsByState('usa')); // outputs ['Ford', 'Dodge']
console.log(getCarsByState('italy')); // outputs ['Fiat']

上面的代码可以重构为不使用switch语句的

const cars = new Map()
  .set('usa', ['Ford', 'Dodge'])
  .set('france', ['Renault', 'Peugeot'])
  .set('italy', ['Fiat']);

const getCarsByState = (state) => {
  return cars.get(state) || [];
}

console.log(getCarsByState()); // outputs []
console.log(getCarsByState('usa')); //outputs ['Ford', 'Dodge']
console.log(getCarsByState('italy')); // outputs ['Fiat']

或者我们可以给每包含可用车的国家创建一个类,在需要的时候调用。不过那就是另一篇文章的主题了。本文只讨论条件判断。一个更适合的方式是使用对象。

const carState = {
  usa: ['Ford', 'Dodge'],
  france: ['Renault', 'Peugeot'],
  italy: ['Fiat']
};

const getCarsByState = (state) => {
  return carState[state] || [];
}

console.log(getCarsByState()); // outputs []
console.log(getCarsByState('usa')); // outputs [‘Ford’, ‘Dodge’]
console.log(getCarsByState('france')); // outputs [‘Renault’, ‘Peugeot’]

7. 自判断链接(Optional Chaining)和空合并(Nullish Coalescing)

这一节我终于可以开始说“最后”了。在我看来,这两个是JavaScript语言中添加的非常有用的功能。作为从C#转来前端的人,我经常使用它们。

在写这篇文章的时候,这两个方法还没有被完全的支持,你需要使用Babel来转换代码。可以查看 optional-chainingNullish coalescing

Optional chaining允许我们在不明确判断某个中间节点是否存在的情况下,处理树结构数据,Nullish coalescing能很好的配合Optional chaining使用,并且能用来对不存在的节点设置默认值。

我们写一下例子来做说明,首先还是用老的方法实现:

const car = {
  model: 'Fiesta',
  manufacturer: {
    name: 'Ford',
    address: {
      street: 'Some Street Name',
      number: '5555',
      state: 'USA'
    }
  }
}

// to get the car model
const model = car && car.model || 'default model';
// to get the manufacturer street
const street = car && car.manufacturer && car.manufacturer.address && car.manufacturer.address.street || 'default street';
// request an un-existing property
const phoneNumber = car && car.manufacturer && car.manufacturer.address && car.manufacturer.phoneNumber;

console.log(model) // outputs ‘Fiesta’
console.log(street) // outputs ‘Some Street Name’
console.log(phoneNumber) // outputs undefined

如果我们想输出生产商是否来自美国,代码可能会这么写:

const checkCarManufacturerState = () => {
  if(car && car.manufacturer && car.manufacturer.address && car.manufacturer.address.state === 'USA') {
    console.log('Is from USA');
  }
}

checkCarManufacturerState() // outputs ‘Is from USA’

我不需再更多的展示在有更复杂的对象结构的时候,代码会变得多么凌乱。很多库,比如lodash,有他们自己的工具函数做替代方案,但是我们想要使用原生的js来实现。让我们看看一种新的写法。

// to get the car model
const model = car?.model ?? 'default model';
// to get the manufacturer street
const street = car?.manufacturer?.address?.street ?? 'default street';

// to check if the car manufacturer is from the USA
const checkCarManufacturerState = () => {
  if(car?.manufacturer?.address?.state === 'USA') {
    console.log('Is from USA');
  }
}

这个看起来更直观更简短,我觉得也更合理。如,果你好奇为什么需要用??代替||,思考一下什么样的值可以等同于true或者false,你会获得意料之外的结果。

另外说个题外话,自判断链接还支持DOM API,非常酷,意味着你可以像这样写代码:

const value = document.querySelector('input#user-name')?.value;

总结

以上就是我想讲的全部内容,如果你喜欢这篇文章,可以请我喝杯咖啡精神一下, 可以订阅或者在Twitter 上面关注我。

感谢阅读,下篇文章见。

Comments
Write a Comment
  • Mygao666 reply

    审美good