Изучаю замену условным конструкциям в JavaScript

Изучаю замену условным конструкциям в JavaScript

Volond

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



// условие
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.

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

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

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

Про деструктуризацию советую почитать тут: Деструктуризация в ES6. Полное руководство

Я полагаю, что код ниже может выглядеть для вас вполне знакомым, нам часто нужно проверять 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)

7 советов по работе с undefined в JavaScript


Вот пример с 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 и объект литералов, тут вы можете её прочитать.

"Рефракторим" код...

Для примера выше, на самом деле, мы можем отрефакторить код с помощью 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
}

Пример использования reduce в качестве замены был подсмотрен у Redux.

Представьте, что у вас есть каталог товаров, у каждого из которых есть определённый набор свойств, в том числе и свойство price, которое и возьмём для примера. Ваш покупатель выбирает товары и добавляет их в корзину. Таким образом, заходя в корзину, пользователь остаётся с выбранными им товарами, которые мы, как разработчики, будем считать массивом объектов подобного вида:

var selected = [
  { price: 20 },
  { price: 45 },
  { price: 67 },
  { price: 1305 }
];



Отлично! Уже сейчас мы можем вычислить общую стоимость товаров и выставить покупателю счёт. Или нет? Магазин у вас интернациональный и каждый клиент вправе выбрать ту валюту, с помощью которой ему будет удобно рассчитаться. Чтобы удобно всё это оформить создадим объект функций “редюсеров”, которые будут вычислять стоимость, исходя из курса рубля:

var reducers = {
  rubles: function(state, item) {
    return state.rubles += item.price;
  },
  dollars: function(state, item) {
    return state.dollars += item.price / 71.6024;
  },
  euros: function(state, item) {
    return state.euros += item.price / 79.0133;
  },
  yens: function(state, item) {
    return state.yens += item.price / 0.6341;
  },
  pounds: function(state, item) {
    return state.pounds += item.price / 101.7829;
  }
};

Получая массив с ценами товаров, мы хотим рассчитать общую цену для каждого типа валюты и на выходе получить объект вида:

var totalPrice = { 
  rubles: 1437,
  dollars: 20.06,
  euros: 18.18,
  yens: 2266.20,
  pounds: 14.15
};


Чтобы получить возможность автоматически использовать все запрашиваемые callback функции из объекта редюсеров нужно написать ещё одну функцию-обёртку, которая будет вычислять все переданные ей значения цен, последовательно вызывая функцию-редюсер для каждого типа валют:

var combineReducers = function(reducers) {
  return function(state, item) {
    return Object.keys(reducers).reduce(function(nextState, key) {
      reducers[key](state, item);
      return state;
    }, {});
  }
};


И это всё, что нам было необходимо сделать. Теперь можем посмотреть, как это всё будет работать:

// Получаем функцию, которая будет всё обрабатывать
var priceReducer = combineReducers(reducers);
// и вычисляем общую стоимость, задавая объект с изначальными значениями
var totalPrice = selected.reduce(priceReducer, {
  rubles: 0, 
  pounds: 0, 
  dollars: 0,
  euros: 0,
  yens: 0
});

console.log(totalPrice);
// {
//   "rubles": 1437,
//   "pounds": 14.118285095040523,
//   "dollars": 20.069159692971187,
//   "euros": 18.186811587416294,
//   "yens": 2266.204068758871
// }


Report Page