5 советов как лучше писать условные конструкции в 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
А теперь, посмотрите на код выше, что у нас есть?
1 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
}