Подписывайтесь на мой твиттер, там всегда что-нибудь интересное!

5 советов как лучше писать условные конструкции в JavaScript

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

Перевод статьи 5 Tips to Write Better Conditionals in JavaScript

Используйте Array.includes в случае с множественным критерием выборки

// условие
function test(fruit) {
  if (fruit == 'apple' || fruit == 'strawberry') {
    console.log('red');
  }
}

На первый взгляд пример выше выглядит вполне хорошо. Однако, что же будет, если у нас станет больше красных фруктов. К примеру ещё добавится вишня (cherry) или клюква (cranberries)? Будем ли мы расширять условие с помощью дополнительных ||?

Мы можем свободно переписать условие, с использованием Array.includes. Смотрите, как это просто:

function test(fruit) {
  //  Выгружаем условия в массив
  const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];

  if (redFruits.includes(fruit)) {
    console.log('red');
  }
}

Тут мы выгружаем список красных фруктов (redFruits) в массив. Так наш код будет выглядеть чище.

Меньше вложений с ранним выходом из функции

Давайте расширим предыдущий пример и включим в него ещё два условия.

Если нет фруктов, то выкинем ошибку.

Выведем в консоль сообщение, что фруктов больше 10.

function test(fruit, quantity) {
  const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];

  //  Условие 1: fruit должен иметь значение
  if (fruit) {
    //  Условие 2: он должен быть красным
    if (redFruits.includes(fruit)) {
      console.log('red');

      //  Условие 3: должно быть большое количество
      if (quantity > 10) {
        console.log('big quantity');
      }
    }
  } else {
    throw new Error('No fruit!');
  }
}

// test results
test(null); // error: No fruits
test('apple'); // print: red
test('apple', 20); // print: red, big quantity

А теперь, посмотрите на код выше, что у нас есть?

if/else, который фильтрует неверные условия

3 уровня вложения (условия 1, 2 и 3)

Главное правило которому лично я стараюсь следовать — это всегда делать ранний выход из функции при обнаружении изначально неверного условия.

/_ Раннее завершение функции, если найдено неверное условие _/

function test(fruit, quantity) {
  const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];

  //  Условие 1: выдача ошибки сразу
  if (!fruit) throw new Error('No fruit!');

  //  Условие 2: должно быть красным
  if (redFruits.includes(fruit)) {
    console.log('red');

    //  Условие 3: большое количество
    if (quantity > 10) {
      console.log('big quantity');
    }
  }
}

Таким образом у нас на один уровень вложения меньше. Такой подход особенно хорош тогда, когда у вас реально длинное условие if (представьте, если бы вам пришлось скролить к самому концу кода, чтобы узнать, есть ли там else. Это же обалдеть можно)

Дальше мы можем ещё больше сократить вложения if, инвертируя условие с ранним выходом из функции. Посмотрите на второе условие ниже и всё увидите:

/_ ранний выход из функции, если найдено неверное условие _/
function test(fruit, quantity) {
  const redFruits = ['apple', 'strawberry', 'cherry', 'cranberries'];

  if (!fruit) throw new Error('No fruit!');  // Условие 1: выдача ошибки сразу
  if (!redFruits.includes(fruit)) return;  Условие 2: стоп, если фрукт не красный

  console.log('red');

  //  Условие 3: количество должно быть больше 10
  if (quantity > 10) {
    console.log('big quantity');
  }
}

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

Однако, это не то правило, которого нужно безукоризненно придерживаться. Спросите себя сами, эта версия (без вложения) лучше/более читабельная, чем предыдущая (второе условие с вложением)?

Лично для меня подходит предыдущая версия (второе условие с вложением). И вот почему:

Код короче и проще, он куда понятнее с вложенным if.

Инвертирование условия может нагрузить в плане логики и заставить лишний раз подумать.

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

Применение дефолтных параметров функции и деструктуризация

Я полагаю, что код ниже может выглядеть для вас вполне знакомым, нам часто нужно проверять null/undefined и назначать дефолтные значения во время работы с JavaScript:

function test(fruit, quantity) {
  if (!fruit) return;
  const q = quantity || 1; //  Если не указывается количество, то по-дефолту будет 1

  console.log(`We have ${q} ${fruit}!`);
}

//test results
test('banana'); // We have 1 banana!
test('apple', 2); // We have 2 apple!

На самом деле мы можем избавиться от переменной q, указав дефолтные параметры функции.

function test(fruit, quantity = 1) { // Если не указывается количество, до по-дефолту будет 1
  if (!fruit) return;
  console.log(`We have ${quantity} ${fruit}!`);
}

//test results
test('banana'); // We have 1 banana!
test('apple', 2); // We have 2 apple!

Куда проще и интуитивно понятнее, не так ли? Пожалуйста, обратите внимание на то, что каждый параметр может иметь своё собственное дефолтное значение. Для примера, мы можем указать дефолтное значение для fruit:

function test(fruit = 'unknown', quantity = 1)

А что если fruit это объект? Можем ли мы назначить дефолтный параметр?

function test(fruit) { 
  //  выводим название фрукта, если его указывают
  if (fruit && fruit.name)  {
    console.log (fruit.name);
  } else {
    console.log('unknown');
  }
}

//test results
test(undefined); // unknown
test({ }); // unknown
test({ name: 'apple', color: 'red' }); // apple

Посмотрите на пример выше, там нам надо вывести имя фрукта, если оно доступно или мы выведем в консоль unknown. Мы можем смело избежать объявления условия fruit && fruit.name с помощью дефолтных параметров функции и деструктуризации.

//  назначаем дефолтный пустой объект {}
function test({name} = {}) {
  console.log (name || 'unknown');
}

//test results
test(undefined); // unknown
test({ }); // unknown
test({ name: 'apple', color: 'red' }); // apple

Так как нам нужно только свойство name от fruit, мы можем деструктуризировать параметр, используя {name}, затем мы можем использовать name, как переменную в нашем коде, вместо fruit.name.

Мы также назначаем пустой объект {}, как дефолтное значение. Если мы так не сделаем, то получим ошибку при выполнении test(undefined) — Cannot destructure property name of ‘undefined’ or ‘null’. Потому что тут нет свойства name в undefined.

Если вы не против использовать сторонние библиотеки, то есть несколько способов сократить проверку null:

Используйте функцию get из Lodash

Используйте idx от Facebook (c Babeljs)

Вот пример с Lodash:

function test(fruit) {
  console.log(__.get(fruit, 'name', 'unknown'); //  Получает name у fruit, если его нет, то назначается ‘unknown’.
}

//test results
test(undefined); // unknown
test({ }); // unknown
test({ name: 'apple', color: 'red' }); // apple

Вы можете запустить демо код тут. Кроме того, если вы фанат Функционального Програмирования, вы можете выбрать Lodash fp, функциональную версию Lodash. Там метод изменится с get на getOr).

Предпочитайте Map / Объект вместо Switch

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

function test(color) {
  //  используйте case, для выбора фруктов по цвету
  switch (color) {
    case 'red':
      return ['apple', 'strawberry'];
    case 'yellow':
      return ['banana', 'pineapple'];
    case 'purple':
      return ['grape', 'plum'];
    default:
      return [];
  }
}

//test results
test(null); // []
test('yellow'); // ['banana', 'pineapple']

Кажется, что в коде выше всё правильно, но мне он кажется довольно громоздким. Тот же результат можно получить с помощью объект литерала и более чистого синтаксиса:

//  применяем объект литерал, чтобы найти фрукты по цвету
  const fruitColor = {
    red: ['apple', 'strawberry'],
    yellow: ['banana', 'pineapple'],
    purple: ['grape', 'plum']
  };

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

Или вы можете использовать Map, чтобы достичь того же результата:

//  используйте Map, чтобы найти фрукты по цвету
  const fruitColor = new Map()
    .set('red', ['apple', 'strawberry'])
    .set('yellow', ['banana', 'pineapple'])
    .set('purple', ['grape', 'plum']);

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

Map это тип объекта, который появился в ES2015 и позволяет вам хранить данные как key value (ключ и его значение).

А теперь вопрос. Нужно ли нам прекратить использование switch? Не ограничивайте себя этим. Лично я использую объект литералы, когда это возможно, но я бы не стал выставлять жесткие ограничения в этом плане, используйте то, что больше подходит под конкретный случай.

У Todd Motto есть интересная статья, в которой он углубляется в вопрос использования switch и объект литералов, тут вы можете её прочитать.

TL;DR Рефакторим

Для примера выше, на самом деле, мы можем отрефакторить код с помощью Array.filter.

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) {
  //  используем Array.filter, чтобы найти фрукты по цвету

  return fruits.filter(f => f.color == color);
}

Всегда есть больше одного способа достигнуть одного и того же результата. Мы увидели 4 для одного и того же примера. В общем, кодить очень даже весело!

Используйте Array.every или Array.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
}

Тут всё так долго, мы вполне можем сократить число строк с помощью Array.every:

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
}

Теперь всё куда опрятнее, да? Таким же образом мы можем проверить есть ли в нашем массиве красные фрукты, для этого мы можем просто использовать Array.some в одну строку.

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

function test() {
  //  Условие: есть ли красные фрукты в массиве
  const isAnyRed = fruits.some(f => f.color == 'red');

  console.log(isAnyRed); // true
}