Изучаю замену условным конструкциям в 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
А теперь, посмотрите на код выше, что у нас есть?
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.
Инвертирование условия может нагрузить в плане логики и заставить лишний раз подумать.
Как вывод, всегда стремитесь к наименьшему вложению и раннему выходу из функции, но не пренебрегайте ими.
Применение дефолтных параметров функции и деструктуризация
Про деструктуризацию советую почитать тут: Деструктуризация в 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
// }