10 важни функции на Lodash за разработчици на JavaScript

За разработчиците на JavaScript Lodash няма нужда от представяне. Въпреки това библиотеката е огромна и често се чувства непосилна. Вече не!

Лодаш, Лодаш, Лодаш. . . откъде изобщо да започна! 🤔

Имаше време, когато екосистемата на JavaScript се зараждаше; може да се сравни с дивия запад или джунглата, ако щете, където се случваха много неща, но имаше много малко отговори за ежедневните разочарования и производителност на разработчиците.

Тогава Лодаш влезе на сцената и се почувства като наводнение, което потапя всичко. От прости ежедневни нужди като сортиране до сложни трансформации на структурата на данни, Lodash дойде зареден (дори претоварен!) с функционалност, която превърна живота на JS разработчиците в истинско блаженство.

Здравей, Лодаш!

И къде е Лодаш днес? Е, той все още има всички екстри, които предлагаше първоначално, а след това и някои, но изглежда е загубил ума си в общността на JavaScript. Защо? Сещам се за няколко причини:

  • Някои функции в библиотеката на Lodash бяха (и все още са) бавни, когато се прилагат към големи списъци. Въпреки че това никога не би засегнало 95% от проектите там, влиятелни разработчици от останалите 5% дадоха лоша преса на Lodash и ефектът каскадно се спусна към обикновените хора.
  • Има тенденция в JS екосистемата (може дори да кажем същото за хората от Golang), където високомерието е по-често срещано от необходимото. Така че разчитането на нещо като Lodash се възприема като глупаво и се отхвърля във форуми като StackOverflow, когато хората предлагат такива решения („Какво?! Използвам цяла библиотека за нещо подобно? Мога да комбинирам filter() с reduce(), за да постигна същото нещо в проста функция!“).
  • Лодаш е стар. Поне по стандартите на JS. Излезе през 2012 г., така че от момента на писане са минали почти десет години. API е стабилен и всяка година не могат да се добавят много вълнуващи неща (просто защото няма нужда), което генерира скука за средностатистическия превъзбуден JS разработчик.

Според мен неизползването на Lodash е значителна загуба за нашите кодови бази на JavaScript. Той има доказано безпроблемни и елегантни решения за ежедневните проблеми, с които се сблъскваме по време на работа, и използването му само ще направи кода ни по-четлив и поддържаем.

С това казано, нека се потопим в някои от често срещаните (или не!) функции на Lodash и да видим колко невероятно полезна и красива е тази библиотека.

Клонинг . . . дълбоко!

Тъй като обектите се предават по препратка в JavaScript, това създава главоболие за разработчиците, когато искат да клонират нещо с надеждата, че новият набор от данни е различен.

let people = [
  {
    name: 'Arnold',
    specialization: 'C++',
  },
  {
    name: 'Phil',
    specialization: 'Python',
  },
  {
    name: 'Percy',
    specialization: 'JS',
  },
];

// Find people writing in C++
let folksDoingCpp = people.filter((person) => person.specialization == 'C++');

// Convert them to 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);

// Find people writing in C++
let folksDoingCpp = peopleCopy.filter(
  (person) => person.specialization == 'C++'
);

// Convert them to 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++). Но по-важното е, че кодът е лесен за разбиране.

  Как да поправите грешката 0x97 на принтерите на Epson

Премахване на дубликати от масив

Премахването на дубликати от масив звучи като отличен проблем с интервю/бела дъска (не забравяйте, че когато се съмнявате, хвърлете hashmap на проблема!). И, разбира се, винаги можете да напишете персонализирана функция, за да направите това, но какво ще стане, ако срещнете няколко различни сценария, в които да направите вашите масиви уникални? Бихте могли да напишете няколко други функции за това (и рискувате да попаднете на фини грешки), или можете просто да използвате 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' },
  ],
};

// find user ids that placed postpaid orders (internal or external)
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` object missing
  processedAt: `2021-10-10 00:00:00`,
};

Както е показано, обикновено получаваме обект за поръчка в отговора от API, но не винаги е така. И така, какво ще стане, ако имаме код, който разчита на този обект? Един от начините би бил да кодираме отбранително, но в зависимост от това колко вложен е обектът на поръчката, скоро ще пишем много грозен код, ако искаме да избегнем грешки по време на изпълнение:

if (
  apiResponse.order &&
  apiResponse.order.payee &&
  apiResponse.order.payee.address
) {
  console.log(
    'The order was sent to the zip code: ' +
      apiResponse.order.payee.address.zipCode
  );
}

🤢🤢 Да, много грозно за писане, много грозно за четене, много грозно за поддържане и т.н. За щастие, Лодаш има лесен начин да се справи с подобни ситуации.

const zipCode = _.get(apiResponse, 'order.payee.address.zipCode');
console.log('The order was sent to the zip code: ' + zipCode);
// The order was sent to the zip code: undefined

Има и фантастичната опция за предоставяне на стойност по подразбиране, вместо да бъде недефинирана за липсващи неща:

const zipCode2 = _.get(apiResponse, 'order.payee.address.zipCode', 'NA');
console.log('The order was sent to the zip code: ' + zipCode2);
// The order was sent to the zip code: NA

Не знам за вас, но get() е едно от онези неща, които предизвикват сълзи от щастие в очите ми. Не е нещо бляскаво. Няма консултиран синтаксис или опции за запаметяване, но вижте количеството колективно страдание, което може да облекчи! 😇

Отскачане

В случай, че не сте запознати, премахването на отскок е често срещана тема при разработката на интерфейса. Идеята е, че понякога е полезно да стартирате действие не веднага, а след известно време (обикновено няколко милисекунди). Какво означава това? Ето един пример.

Представете си уебсайт за електронна търговия с лента за търсене (е, всеки уебсайт/уеб приложение в наши дни!). За по-добър UX не искаме потребителят да трябва да натиска enter (или по-лошо, да натиска бутона „търсене“), за да показва предложения/предварителни прегледи въз основа на тяхната дума за търсене. Но очевидният отговор е малко натоварен: ако добавим слушател на събития към onChange() за лентата за търсене и задействаме извикване на API за всяко натискане на клавиш, щяхме да създадем кошмар за нашия бекенд; ще има твърде много ненужни обаждания (например, ако се търси „четка за бял килим“, ще има общо 18 заявки!) и почти всички от тях ще бъдат неуместни, защото въвеждането от потребителя не е приключило.

Отговорът се крие в премахването на отскок и идеята е следната: не изпращайте API извикване веднага щом текстът се промени. Изчакайте известно време (да речем 200 милисекунди) и ако до този момент има друго натискане на клавиш, отменете по-ранното отчитане на времето и започнете отново да чакате. В резултат на това само когато потребителят спре (или защото мисли, или защото е свършил и очаква някакъв отговор), ние изпращаме API заявка към бекенда.

Цялостната стратегия, която описах, е сложна и няма да се задълбочавам в синхронизирането на управлението и анулирането на таймера; Въпреки това, действителният процес на премахване на отскок е много прост, ако използвате Lodash.

const _ = require('lodash');
const axios = require('axios');

// This is a real dogs' API, by the way!
const fetchDogBreeds = () =>
  axios
    .get('https://dog.ceo/api/breeds/list/all')
    .then((res) => console.log(res.data));

const debouncedFetchDogBreeds = _.debounce(fetchDogBreeds, 1000); // after one second
debouncedFetchDogBreeds(); // shows data after some time

Ако си мислите setTimeout() щях да свърша същата работа, е, има още! Debounce на Lodash идва с много мощни функции; например, може да искате да се уверите, че отблъскването не е неопределено. Тоест, дори ако има натискане на клавиш всеки път, когато функцията е на път да се задейства (като по този начин отменя цялостния процес), може да искате да се уверите, че извикването на API е направено така или иначе след, да речем, две секунди. За това Lodash debounce() има опцията maxWait:

const debouncedFetchDogBreeds = _.debounce(fetchDogBreeds, 150, { maxWait: 2000 }); // debounce for 250ms, but send the API request after 2 seconds anyway

Проверете официалния документи за по-дълбоко гмуркане. Те са пълни със супер важни неща!

  Как да защитим части от Word документ от редактиране

Премахване на стойности от масив

Не знам за вас, но аз мразя да пиша код за премахване на елементи от масив. Първо, трябва да получа индекса на елемента, да проверя дали индексът действително е валиден и ако е така, да извикам метода splice() и т.н. Никога не мога да си спомня синтаксиса и затова трябва да търся нещата през цялото време и накрая оставам с неприятното усещане, че съм допуснал някоя глупава грешка да се промъкне.

const greetings = ['hello', 'hi', 'hey', 'wave', 'hi'];
_.pull(greetings, 'wave', 'hi');
console.log(greetings);
// [ 'hello', 'hey' ]

Моля, обърнете внимание на две неща:

  • Оригиналният масив беше променен в процеса.
  • Методът pull() премахва всички екземпляри, дори ако има дубликати.
  • Има друг свързан метод, наречен pullAll(), който приема масив като втори параметър, което улеснява премахването на няколко елемента наведнъж. Разбира се, че можем просто да използваме pull() с оператор за разпространение, но не забравяйте, че Lodash дойде във време, когато операторът за разпространение дори не беше предложение в езика!

    const greetings2 = ['hello', 'hi', 'hey', 'wave', 'hi'];
    _.pullAll(greetings2, ['wave', 'hi']);
    console.log(greetings2);
    // [ 'hello', 'hey' ]

    Последен индекс на елемент

    Родният метод indexOf() на JavsScript е готин, освен когато се интересувате от сканиране на масива от обратната посока! И все пак, да, можете просто да напишете намаляващ цикъл и да намерите елемента, но защо не използвате много по-елегантна техника?

    Ето едно бързо решение на 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' ] ]

    Помощната програма за компресиране/разархивиране не е нещо, което ще промени живота ви за една нощ, но ще промени живота ви един ден!

    Заключение 👨‍🏫

    (Поставям целия изходен код, използван в тази статия тук за да опитате директно от браузъра!)

    Лодашът документи са пълни с примери и функции, които просто ще ви взривят ума. Във времена, в които мазохизмът изглежда нараства в JS екосистемата, Lodash е като глътка свеж въздух и горещо ви препоръчвам да използвате тази библиотека във вашите проекти!