Для розробників, що працюють з JavaScript, Lodash — це знайоме ім’я. Однак, ця бібліотека є досить великою, і часто може здаватися надмірною. Але це вже в минулому!
Lodash, Lodash, Lodash… З чого ж почати? 🤔
Були часи, коли екосистема JavaScript тільки починала розвиватися. Її можна було порівняти з Диким Заходом або джунглями, де багато чого відбувалося, але було дуже мало відповідей на щоденні виклики розробників та підвищення їхньої продуктивності.
А потім з’явився Lodash, і це було схоже на повінь, яка охопила все. Від простих щоденних потреб, таких як сортування, до складних трансформацій структур даних, Lodash був наповнений (навіть переповнений!) функціоналом, який перетворив життя JS-розробників на справжнє задоволення.
Привіт, Lodash!
А де ж Lodash сьогодні? Що ж, він все ще має всі переваги, які пропонував спочатку, і навіть деякі нові, але, здається, він втратив свою частку в спільноті JavaScript. Чому? Я можу назвати декілька причин:
- Деякі функції в бібліотеці Lodash працювали (і залишаються) повільними при обробці великих списків. Хоча це ніколи не вплинуло б на 95% проектів, впливові розробники з решти 5% негативно відгукнулися про Lodash в пресі, і це вплинуло на широку аудиторію.
- В екосистемі JS існує тенденція (можна навіть сказати те саме про розробників Golang), де зарозумілість є більш поширеною, ніж це необхідно. Тому покладатися на щось на зразок Lodash вважається нерозумним, і на таких форумах, як StackOverflow, його критикують, коли люди пропонують такі рішення («Що?! Використовувати цілу бібліотеку для чогось подібного? Я можу поєднати filter() з reduce(), щоб досягти того самого в простій функції!»).
- Lodash – це стара бібліотека, принаймні за мірками JS. Вона з’явилася в 2012 році, тому на момент написання минуло майже десять років. API був стабільним, і не можна додавати багато цікавих нових можливостей щороку (просто тому, що в цьому немає потреби), що викликає нудьгу у середньостатистичного, надмірно збудженого JS-розробника.
На мій погляд, відмова від Lodash є значною втратою для наших кодових баз JavaScript. Він містить перевірені елегантні рішення без помилок для щоденних проблем, з якими ми стикаємося в роботі, і його використання лише зробить наш код більш читабельним та зручним для підтримки.
З огляду на це, давайте розглянемо деякі з поширених (або не дуже!) функцій Lodash і побачимо, наскільки ця бібліотека неймовірно корисна та елегантна.
Клонування… Глибоко!
Оскільки в JavaScript об’єкти передаються за посиланням, це створює головний біль для розробників, коли вони хочуть щось клонувати з надією, що новий набір даних буде відрізнятися.
let people = [ { name: 'Arnold', specialization: 'C++', }, { name: 'Phil', specialization: 'Python', }, { name: 'Percy', specialization: 'JS', }, ]; // Знайти людей, що пишуть на C++ let folksDoingCpp = people.filter((person) => person.specialization == 'C++'); // Перетворити їх на JS! for (person of folksDoingCpp) { person.specialization = 'JS'; } console.log(folksDoingCpp); // [ { name: 'Arnold', specialization: 'JS' } ] console.log(people); /* [ { name: 'Arnold', specialization: 'JS' }, { name: 'Phil', specialization: 'Python' }, { name: 'Percy', specialization: 'JS' } ] */
Зверніть увагу, як з нашої чистої наївності, та попри наші добрі наміри, вихідний масив людей змінився в процесі (спеціалізація Арнольда змінилася з C++ на JS) – це серйозний удар по цілісності базової системи програмного забезпечення! Дійсно, нам потрібен спосіб зробити справжню (глибоку) копію вихідного масиву.
Можливо, ви можете заперечити, що це «дурний» спосіб кодування в JS, проте реальність є дещо складнішою. Так, у нас є чудовий оператор деструктуризації, але будь-хто, хто намагався деструктурувати складні об’єкти та масиви, знає про це біль. Крім того, існує ідея використання серіалізації та десеріалізації (можливо, JSON) для досягнення глибокого копіювання, але це лише ускладнює ваш код для читача.
Натомість, подивіться, яке неймовірно елегантне та лаконічне рішення, коли використовується Lodash:
const _ = require('lodash'); let people = [ { name: 'Arnold', specialization: 'C++', }, { name: 'Phil', specialization: 'Python', }, { name: 'Percy', specialization: 'JS', }, ]; let peopleCopy = _.cloneDeep(people); // Знайти людей, що пишуть на C++ let folksDoingCpp = peopleCopy.filter( (person) => person.specialization == 'C++' ); // Перетворити їх на JS! for (person of folksDoingCpp) { person.specialization = 'JS'; } console.log(folksDoingCpp); // [ { name: 'Arnold', specialization: 'JS' } ] console.log(people); /* [ { name: 'Arnold', specialization: 'C++' }, { name: 'Phil', specialization: 'Python' }, { name: 'Percy', specialization: 'JS' } ] */
Зверніть увагу, як масив людей залишається недоторканим після глибокого клонування (у цьому випадку Арнольд все ще спеціалізується на C++). Але що важливіше, код простий для розуміння.
Видалення дублікатів з масиву
Видалення дублікатів з масиву звучить як чудова проблема для співбесіди (пам’ятайте, якщо сумніваєтеся, додайте хеш-карту до проблеми!). І, звичайно, ви завжди можете написати спеціальну функцію для цього, але що, якщо ви зіткнетеся з кількома різними сценаріями, в яких ваші масиви будуть різними? Ви можете написати кілька інших функцій для цього (і ризикувати зіткнутися з тонкими помилками), або ви можете просто використовувати Lodash!
Наш перший приклад унікальних масивів досить тривіальний, але він все ще демонструє швидкість та надійність, яку привносить Lodash. Уявіть собі, що ви робите це, власноруч створюючи всю спеціальну логіку!
const _ = require('lodash'); const userIds = [12, 13, 14, 12, 5, 34, 11, 12]; const uniqueUserIds = _.uniq(userIds); console.log(uniqueUserIds); // [ 12, 13, 14, 5, 34, 11 ]
Зверніть увагу, що остаточний масив не сортується, що, звичайно, тут не має значення. Але тепер давайте уявимо складніший сценарій: у нас є масив користувачів, який ми звідкись отримали, але ми хочемо переконатися, що він містить лише унікальних користувачів. Легко з Lodash!
const _ = require('lodash'); const users = [ { id: 10, name: 'Phil', age: 32 }, { id: 8, name: 'Jason', age: 44 }, { id: 11, name: 'Rye', age: 28 }, { id: 10, name: 'Phil', age: 32 }, ]; const uniqueUsers = _.uniqBy(users, 'id'); console.log(uniqueUsers); /* [ { id: 10, name: 'Phil', age: 32 }, { id: 8, name: 'Jason', age: 44 }, { id: 11, name: 'Rye', age: 28 } ] */
У цьому прикладі ми використали метод uniqBy(), щоб повідомити Lodash, що ми хочемо, щоб об’єкти були унікальними за властивістю id. В одному рядку ми висловили те, що могло б зайняти 10-20 рядків, і представили більше можливостей для помилок!
У Lodash є багато іншого для роботи з унікальністю, і я рекомендую вам ознайомитися з документацією.
Різниця між двома масивами
Об’єднання, різниця тощо можуть звучати як терміни, які краще залишити позаду в нудних лекціях з теорії множин у середній школі, але вони частіше з’являються у повсякденній практиці. Зазвичай є список, і ви хочете об’єднати його з іншим списком, або хочете знайти, які елементи є унікальними для нього порівняно з іншим списком. Для цих сценаріїв функція різниці ідеальна.
Давайте розпочнемо нашу подорож з простого сценарію: ви отримали список усіх ідентифікаторів користувачів у системі, а також список тих, чиї облікові записи активні. Як знайти неактивні ідентифікатори? Просто, правда?
const _ = require('lodash'); const allUserIds = [1, 3, 4, 2, 10, 22, 11, 8]; const activeUserIds = [1, 4, 22, 11, 8]; const inactiveUserIds = _.difference(allUserIds, activeUserIds); console.log(inactiveUserIds); // [ 3, 2, 10 ]
А що, якщо, як це буває в більш реалістичному середовищі, вам доведеться працювати з масивом об’єктів замість простих примітивів? Що ж, для цього в Lodash є корисний метод differenceBy()!
const allUsers = [ { id: 1, name: 'Phil' }, { id: 2, name: 'John' }, { id: 3, name: 'Rogg' }, ]; const activeUsers = [ { id: 1, name: 'Phil' }, { id: 2, name: 'John' }, ]; const inactiveUsers = _.differenceBy(allUsers, activeUsers, 'id'); console.log(inactiveUsers); // [ { id: 3, name: 'Rogg' } ]
Чудово, чи не так?!
Подібно до різниці, у Lodash існують інші методи для операцій над множинами: об’єднання, перетин тощо.
Зведення масивів
Необхідність зведення масивів виникає досить часто. Одним із випадків використання є те, що ви отримали відповідь від API, і вам потрібно застосувати деяку комбінацію map() і filter() до складного списку вкладених об’єктів/масивів, щоб вилучити, скажімо, ідентифікатори користувачів, і тепер у вас залишаються масиви масивів. Ось фрагмент коду, який описує цю ситуацію:
const orderData = { internal: [ { userId: 1, date: '2021-09-09', amount: 230.0, type: 'prepaid' }, { userId: 2, date: '2021-07-07', amount: 130.0, type: 'prepaid' }, ], external: [ { userId: 3, date: '2021-08-08', amount: 30.0, type: 'postpaid' }, { userId: 4, date: '2021-06-06', amount: 330.0, type: 'postpaid' }, ], }; // знайти ідентифікатори користувачів, які розмістили післяоплачені замовлення (внутрішні або зовнішні) const postpaidUserIds = []; for (const [orderType, orders] of Object.entries(orderData)) { postpaidUserIds.push(orders.filter((order) => order.type === 'postpaid')); } console.log(postpaidUserIds);
Чи можете ви здогадатися, як зараз виглядає postPaidUserIds? Підказка: це жахливо!
[ [], [ { userId: 3, date: '2021-08-08', amount: 30, type: 'postpaid' }, { userId: 4, date: '2021-06-06', amount: 330, type: 'postpaid' } ] ]
Тепер, якщо ви розумна людина, ви не захочете писати спеціальну логіку, щоб витягти об’єкти замовлення та красиво розмістити їх у рядку всередині масиву. Просто використовуйте метод flatten() і насолоджуйтеся:
const flatUserIds = _.flatten(postpaidUserIds); console.log(flatUserIds); /* [ { userId: 3, date: '2021-08-08', amount: 30, type: 'postpaid' }, { userId: 4, date: '2021-06-06', amount: 330, type: 'postpaid' } ] */
Зверніть увагу, що flatten() заглиблюється лише на один рівень. Тобто, якщо ваші об’єкти застрягли на двох, трьох або більше рівнях глибини, flatten() вас розчарує. У таких випадках Lodash має метод flattenDeep(), але майте на увазі, що застосування цього методу до дуже великих структур може сповільнити роботу (оскільки за лаштунками працює рекурсивна операція).
Чи порожній об’єкт/масив?
Через те, як «хибні» значення та типи працюють у JavaScript, іноді щось таке просте, як перевірка на порожнечу, призводить до екзистенційного страху.
Як перевірити, чи порожній масив? Ви можете перевірити, чи дорівнює його довжина 0, чи ні. Тепер, як перевірити, чи об’єкт порожній? Ну… зачекайте! Саме тут виникає відчуття дискомфорту, і приклади JavaScript, які містять такі речі, як [] == false і {} == false починають обертатися в наших головах. Коли ви відчуваєте тиск, щоб забезпечити певну функціональність, такі підводні камені — останнє, що вам потрібно — вони ускладнять розуміння вашого коду та внесуть невизначеність у ваш набір тестів.
Робота з відсутніми даними
У реальному світі дані не завжди ідеальні. Незалежно від того, як сильно ми цього хочемо, рідко вони бувають спрощеними та логічними. Типовим прикладом є відсутність нульових об’єктів/масивів у великій структурі даних, отриманій як відповідь API.
Припустимо, ми отримали такий об’єкт як відповідь API:
const apiResponse = { id: 33467, paymentRefernce: 'AEE3356T68', // об'єкт order відсутній processedAt: `2021-10-10 00:00:00`, };
Як показано, зазвичай ми отримуємо об’єкт замовлення у відповіді від API, але це не завжди так. Отже, що, якщо у нас є код, який покладається на цей об’єкт? Одним зі способів було б кодувати захисний код, але залежно від того, наскільки вкладеним є об’єкт замовлення, ми скоро почнемо писати дуже потворний код, якщо хочемо уникнути помилок під час виконання:
if ( apiResponse.order && apiResponse.order.payee && apiResponse.order.payee.address ) { console.log( 'Замовлення було відправлено на поштовий індекс: ' + apiResponse.order.payee.address.zipCode ); }
🤢🤢 Так, дуже негарно писати, дуже негарно читати, дуже негарно підтримувати і так далі. На щастя, Lodash має простий спосіб вирішення таких ситуацій.
const zipCode = _.get(apiResponse, 'order.payee.address.zipCode'); console.log('Замовлення було відправлено на поштовий індекс: ' + zipCode); // Замовлення було відправлено на поштовий індекс: undefined
Існує також чудова можливість надати значення за замовчуванням замість того, щоб отримувати значення undefined для відсутніх речей:
const zipCode2 = _.get(apiResponse, 'order.payee.address.zipCode', 'NA'); console.log('Замовлення було відправлено на поштовий індекс: ' + zipCode2); // Замовлення було відправлено на поштовий індекс: NA
Я не знаю, як ви, але get() – це одна з тих речей, які викликають у мене сльози щастя. Це не щось видатне. Немає особливого синтаксису чи варіантів для запам’ятовування, але подивіться, скільки колективних страждань це може полегшити! 😇
Усунення стрибків
Якщо ви не знайомі, усунення стрибків є поширеною темою в розробці інтерфейсів. Ідея полягає в тому, що іноді корисно запускати дію не одразу, а через деякий час (зазвичай, кілька мілісекунд). Що це означає? Ось приклад.
Уявіть веб-сайт електронної комерції з панеллю пошуку (ну, будь-який веб-сайт/веб-програма в наші дні!). Для кращого UX ми не хочемо, щоб користувачеві доводилося натискати Enter (або, що ще гірше, натискати кнопку «пошук»), щоб показати пропозиції/попередній перегляд на основі їхнього пошукового запиту. Але очевидна відповідь дещо обтяжена: якщо ми додамо прослуховувач подій до onChange() для панелі пошуку і будемо запускати виклик API для кожного натискання клавіші, ми створимо кошмар для нашої серверної частини. Буде занадто багато непотрібних викликів (наприклад, якщо буде введено пошук “щітка для білого килима”, буде загалом 18 запитів!), і майже всі вони будуть нерелевантними, оскільки введення користувача не завершено.
Відповідь полягає в усуненні стрибків, і ідея така: не надсилайте виклик API, як тільки зміниться текст. Зачекайте деякий час (скажімо, 200 мілісекунд), і якщо до цього часу буде ще одне натискання клавіші, скасуйте попередній відлік часу і знову почніть очікування. У результаті лише тоді, коли користувач зупиняється (або тому, що він думає, або тому, що він закінчив і очікує відповіді), ми надсилаємо запит API на сервер.
Загальна стратегія, яку я описав, є складною, і я не буду занурюватися в синхронізацію керування таймером і скасування. Проте фактичний процес усунення стрибків дуже простий, якщо ви використовуєте Lodash.
const _ = require('lodash'); const axios = require('axios'); // Це справжній API для собак, до речі! const fetchDogBreeds = () => axios .get('https://dog.ceo/api/breeds/list/all') .then((res) => console.log(res.data)); const debouncedFetchDogBreeds = _.debounce(fetchDogBreeds, 1000); // після однієї секунди debouncedFetchDogBreeds(); // показує дані через деякий час
Якщо ви думаєте про setTimeout(), який може виконати ту саму роботу, то є ще дещо! Debounce від Lodash має багато потужних функцій. Наприклад, ви можете переконатися, що усунення дребезгу не є невизначеним. Тобто, навіть якщо є натискання клавіші кожного разу, коли функція збирається запуститися (таким чином скасовуючи загальний процес), ви можете переконатися, що виклик API все одно виконується через, скажімо, дві секунди. Для цього Lodash debounce() має параметр maxWait:
const debouncedFetchDogBreeds = _.debounce(fetchDogBreeds, 150, { maxWait: 2000 }); // debounce на 250 мс, але надіслати API-запит через 2 секунди
Ознайомтеся з офіційною документацією для глибшого занурення. Там повно корисних речей!
Видалення значень з масиву
Я не знаю, як ви, але я ненавиджу писати код для видалення елементів з масиву. По-перше, я маю отримати індекс елемента, перевірити, чи дійсно індекс дійсний, і якщо так, викликати метод splice() і так далі. Я ніколи не можу запам’ятати синтаксис, і тому мені потрібно постійно щось шукати, і в кінці мене залишає неприємне відчуття, що я дозволив якійсь дурній помилці закрастися.
const greetings = ['hello', 'hi', 'hey', 'wave', 'hi']; _.pull(greetings, 'wave', 'hi'); console.log(greetings); // [ 'hello', 'hey' ]
Зверніть увагу на дві речі:
Існує інший пов’язаний метод під назвою pullAll(), який приймає масив як другий параметр, що полегшує видалення кількох елементів одночасно. Зрозуміло, що ми могли просто використовувати pull() з оператором поширення, але пам’ятайте, що Lodash з’явився в той час, коли оператор поширення навіть не був пропозицією в мові!
const greetings2 = ['hello', 'hi', 'hey', 'wave', 'hi']; _.pullAll(greetings2, ['wave', 'hi']); console.log(greetings2); // [ 'hello', 'hey' ]
Останній індекс елемента
Власний метод JavsScript indexOf() — це чудово, за винятком випадків, коли вам потрібно сканувати масив у зворотному напрямку! І знову ж таки, так, ви можете просто написати цикл зменшення і знайти елемент, але чому б не використати набагато більш елегантний підхід?
Ось швидке рішення Lodash за допомогою методу lastIndexOf():
const integers = [2, 4, 1, 6, -1, 10, 3, -1, 7]; const index = _.lastIndexOf(integers, -1); console.log(index); // 7
На жаль, немає варіанту цього методу, за якого ми можемо шукати складні об’єкти або навіть передати спеціальну функцію пошуку.
Zip. Розпакуйте!
Якщо ви не працювали з Python, zip/unzip – це утиліта, яку ви можете ніколи не помітити або не уявити за всю свою кар’єру розробника JavaScript. І, мабуть, з поважної причини: рідко виникає така відчайдушна потреба в zip/unzip, як у filter() тощо. Однак це одна з найкращих маловідомих утиліт, яка може допомогти вам створити стислий код у деяких ситуаціях.
На відміну від того, як це звучить, zip/unzip не має нічого спільного зі стисненням. Натомість це операція групування, коли масиви однакової довжини можуть бути перетворені в єдиний масив масивів з елементами в одній позиції, згрупованими разом (zip()), і назад (unzip()). Так, я знаю, стає туманно намагатися обійтися словами, тому давайте подивимося на код:
const animals = ['duck', 'sheep']; const sizes = ['small', 'large']; const weight = ['less', 'more']; const groupedAnimals = _.zip(animals, sizes, weight); console.log(groupedAnimals); // [ [ 'duck', 'small', 'less' ], [ 'sheep', 'large', 'more' ] ]
Початкові три масиви було перетворено в єдиний лише з двох масивів. І кожен із цих нових масивів представляє одну тварину з усіма її даними в одному місці. Отже, індекс 0 говорить нам про тип тварини, індекс 1 говорить нам про її розмір, а індекс 2 говорить нам про її вагу. У результаті з даними тепер легше працювати. Після того, як ви застосували будь-які операції, які вам потрібні, ви можете розбити їх знову за допомогою unzip() і відправити назад у вихідне джерело:
const animalData = _.unzip(groupedAnimals); console.log(animalData); // [ [ 'duck', 'sheep' ], [ 'small', 'large' ], [ 'less', 'more' ] ]
Утиліта zip/unzip не змінить ваше життя миттєво, але одного дня вона може стати вам у пригоді!
Висновок 👨🏫
(Я розмістив увесь вихідний код, використаний у цій статті тут, щоб ви могли спробувати прямо у своєму браузері!)
Документація Lodash переповнена прикладами та функціями, які вас просто вразять. У наш час, коли мазохізм, здається, зростає в екосистемі JS, Lodash — це як ковток свіжого повітря, і я наполегливо рекомендую вам використовувати цю бібліотеку у своїх проектах!