Pavel Nakonechnyy

Советы по улучшению условий в JavaScript

Опубликовано by Pavel Nakonechnyy on (изменено: ) в Web development.

Перевод статьи Milos Protic

Если вы любите видеть чистый код, как и я, то вы точно будете пытаться максимально уменьшить свои условия для if’ов. В целом, ООП позволяет нам избегать условий и заменить их полиморфизмом и наследованием. И я верю, что мы должны придерживаться этих принципов.

С другой стороны, так или иначе, мы можем встретиться с условиями в нашем коде. Возможно, нас поджимали дедлайны, или замена условия вызовет слишком много изменений кодовой базы. И эта статья написана с целью, чтобы помочь вам пробраться через эти случаи и организовать ваши условия.

Сначала о главном: просто, но не тривиально

Не используйте негативные условия (они могут вызывать смятение) и используйте сокращения для 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...
}

Теперь, когда мы определились с банальными вещами

Для одного из множества используйте Array.includes

Допустим, мы хотим проверить если модель машины renault или peugeot в нашей функции:

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

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

Учитывая, что мы имеем только две модели, это может выглядеть адекватно, но что если мы захотим проверить третью модель? А еще парочку? И чем больше условий мы будем добавлять, тем сложнее будет поддерживать этот код, поэтому перепишем код с использованием Array.includes:

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'

Теперь, если мы захотим добавить модели, то нам достаточно добавить их в массив. И если это необходимо, мы можем вынести константу моделей из функции и переиспользовать ее где-то в ином месте. Несколько простейших изменений и мы сделали поддержку кода элементарной.

Для проверки всех критериев используйте Array.every или Array.find

Теперь мы хотим проверить, если все машины совпадают по модели с условием. Для достижения этого, мы могли бы написать что-то вроде этого:

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 для достижения того же результата:

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

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

Или Array.find:

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

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

При этом производительность не пострадала, т.к. оба метода по очереди проверяют элементы массива и возвращают false как только один из них не прошел.

Для частичного совпадения условий используйте Array.some

Аналогично тому, как Array.every проверяет все элементы, этот метод проверяет если массив содержит хотя бы один подходящий элемент.

Мы можем сделать то же самое с обычным for циклом, как в одном из примеров выше, но благо у нас есть крутые функции 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

Return вместо ветвления if…else

Когда я был студентом, меня учили, что у функции должно быть только один возврат, и осуществляться он должен из одного место. Это не плохое решение, если относиться к нему с вниманием, т.е. мы должны отслеживать ситуации, когда это приведет к аду из вложенных условий.

С другой стороны, если кодовая база большая и содержит много строк кода, return где-то в глубине может вызвать множество проблем. Но мы придерживаемся принципов 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.

Мы можем переделать функцию выше, сделав ее гораздо более компактной и понятной, используя тернарные операторы, && или другими способами. Но я пропущу всё это и покажу, как использовать современный функционал JS для максимального упрощения кода:

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

  // here we are free to do whatever we want with the model or year
  // we made sure that they exist
  // no more checks required

  // 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, то мы получим ошибку. И это преимущество предыдущего метода, поскольку тогда мы бы получили ожидаемый результат «No car».

Деструктуризация объекта гарантирует, что функция получает только то, что ей нужно. Например, если мы включим еще одно свойство в объект машины, то функция его не получит.

В зависимости от предпочтений, разработчики выберут один из этих подходов. Практика показала мне, что обычно код выглядит как что-то среднее. Многие люди считают,, что if…else проще понять, что помогает им отслеживать работу программы с меньшими затруднениями.

Используйте индексы объектов или Maps вместо 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']

Используйте Optional Chaining и Nullish Coalescing

Эту секцию я могу начать со слов «В конце концов». По моему скромному мнению, эти две функции стали очень полезными дополнениями для JavaScript. Как человек, пришедший из мира C#, я использую их довольно часто.

На момент написания этой статьи, эти фичи еще не полностью поддерживаются, и вам нужен Babel для компиляции кода с их использованием. Состояние функции optional chaining и nullish coalescing.

Опциональная цепочка вызовов позволяет нам обрабатывать древоподобные структуры без проверки существования конкретных элементов. И она отлично срабатывается с однострочной проверкой на null, которая позволит использовать значение по умолчанию вместо несуществующих.

Давайте подкрепим это примерами и начнём с классическим способом реализации:

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, обзавелись собственными функциями для решения этой проблемы. Но нам это не нужно, мы хотим сделать это в vanilla 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, и какие неожиданные последствия это может вызвать.

Еще одна прикольная фича optional chaining, хоть она и не очень в тему: она поддерживает API DOM, что позволяет нам сделать что-то типа:

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

Заключение

Вот и всё, что я нашел сказать по теме.

Если вам понравилась статья, подписывайтесь на нас в ВК или Twitter

Спасибо за прочтение, увидимся в следующей статье!

1 201