10 Důležité funkce Lodash pro vývojáře JavaScriptu

Pro ty, kdo se věnují vývoji v JavaScriptu, není knihovnu Lodash potřeba nijak zvlášť představovat. Nicméně, její rozsah může být pro mnohé ohromující. To už ale není překážka!

Lodash, Lodash, Lodash… Kde jen začít? 🤔

V raných dobách vývoje JavaScriptu byl ekosystém spíše jako divoký západ nebo džungle, kde se sice hodně dělo, ale chyběly efektivní odpovědi na každodenní problémy a frustrace vývojářů. Produktivita trpěla.

Poté se na scéně objevil Lodash a zasáhl jako povodeň. Od jednoduchých úkolů, jako je třídění, až po komplexní transformace dat, Lodash nabídl (a možná i přehnal!) funkce, které pro vývojáře JS znamenaly skutečnou spásu.

Vítejte, Lodashi!

A kde se Lodash nachází dnes? Stále nabízí všechny funkce jako dřív, ale zdá se, že v komunitě JavaScriptu ztratil na popularitě. Proč? Napadá mě několik důvodů:

  • Některé funkce v Lodash byly (a stále jsou) pomalé při práci s velkými datovými sadami. I když to neovlivnilo většinu projektů, kritika od vlivných vývojářů z menší části komunity poškodila reputaci Lodashe.
  • V ekosystému JS (a možná i mezi vývojáři Golangu) se setkáváme s určitou dávkou arogance. Spoléhání se na knihovnu jako Lodash je považováno za „hloupé“. Na fórech jako StackOverflow se setkáte s kritikou, když někdo navrhne takové řešení („Proč bys pro tohle použil celou knihovnu? Můžeš přece zkombinovat filtr() s redukovat() a dosáhnout stejného výsledku!“).
  • Lodash je považován za zastaralý, alespoň podle standardů JS. Vznikl v roce 2012, což je téměř deset let. Jeho API je stabilní a nepřidávají se každým rokem nové vzrušující funkce, protože to není nutné. To může vést k nudě u některých vývojářů.

Podle mého názoru je odmítání Lodashe velká škoda pro naše JavaScriptové projekty. Jeho funkce jsou prověřené a nabízí elegantní řešení pro běžné problémy, se kterými se setkáváme. Jeho používání zlepší čitelnost a udržovatelnost kódu.

Pojďme se podívat na některé běžné (i méně známé) funkce Lodashe a zjistíme, jak užitečná a elegantní tato knihovna je.

Klonování… do hloubky!

Protože v JavaScriptu se objekty předávají odkazem, vývojářům často způsobuje potíže jejich klonování. Zvláště pokud si přejí, aby nová kopie dat byla zcela nezávislá.

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

  // Najdeme lidi, kteří pracují v C++
  let folksDoingCpp = people.filter((person) => person.specialization == 'C++');

  // Převedeme je na 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' }
  ]
  */
  

Všimněte si, jak se původní pole „people“ změnilo. Arnoldova specializace se změnila z C++ na JS. To je špatné pro integritu celého systému. Potřebujeme způsob, jak vytvořit věrnou (hlubokou) kopii původního pole.

Možná byste mohli namítnout, že toto je „hloupý“ způsob, jak v JS kódovat. Realita je ale složitější. Sice máme destrukční operátor, ale ten nefunguje dobře se složitými objekty a poli. Další možností je serializace a deserializace (např. pomocí JSON), ale kód bude méně čitelný.

Podívejte se, jak elegantní a stručné je řešení s Lodashem:

  const _ = require('lodash');

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

  let peopleCopy = _.cloneDeep(people);

  // Najdeme lidi, kteří pracují v C++
  let folksDoingCpp = peopleCopy.filter(
    (person) => person.specialization == 'C++'
  );

  // Převedeme je na 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' }
  ]
  */
  

Pole „people“ zůstalo po klonování nezměněno (Arnold se stále specializuje na C++). A co je důležité, kód je srozumitelný.

Odstranění duplikátů z pole

Odstranění duplikátů z pole vypadá jako dobrý problém k řešení během pohovoru. Samozřejmě můžete napsat vlastní funkci, ale co když máte několik různých scénářů? Můžete napsat další funkce (a riskovat chyby), nebo použít Lodash!

První příklad je jednoduchý, ale ukazuje rychlost a spolehlivost Lodashe. Představte si, že byste museli napsat všechnu logiku sami!

  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 ]
  

Výsledné pole není seřazeno, ale to není problém. Nyní si představme složitější scénář: máme pole uživatelů, které jsme stáhli, ale chceme jen jedinečné uživatele. Snadné s Lodashem!

  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 }
  ]
  */
  

Použili jsme metodu uniqBy() a řekli Lodashovi, že chceme unikátní objekty podle vlastnosti „id“. Jediný řádek kódu vyřešil to, co by jinak zabralo 10-20 řádků s rizikem chyb!

V Lodashi je mnoho dalších nástrojů pro práci s unikátními prvky. Doporučuji vám prozkoumat dokumentaci.

Rozdíl dvou polí

Sjednocení, rozdíl atd. mohou znít jako pojmy z nudných přednášek z teorie množin, ale v praxi se objevují velmi často. Je běžné, že máte seznam a chcete ho zkombinovat s jiným seznamem, nebo zjistit, které prvky jsou jedinečné. Pro tyto scénáře se hodí funkce pro rozdíl.

Začněme s jednoduchým scénářem: máte seznam ID všech uživatelů v systému a seznam těch, kteří jsou aktivní. Jak zjistíte neaktivní ID?

  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 ]
  

A co když, jak se stává v reálných situacích, musíte pracovat s objekty místo primitiv? Lodash má metodu 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' } ]
  

Skvělé, že?!

Kromě rozdílu existují i další metody pro práci s množinami: sjednocení, průnik atd.

Zploštění polí

Potřeba zploštit pole se objevuje poměrně často. Například když obdržíte odpověď API a potřebujete zkombinovat metody map() a filter() pro složitý seznam objektů, abyste získali například ID uživatelů. Nakonec získáte pole polí. Zde je příklad:

  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' },
    ],
  };

  // najdeme ID uživatelů, kteří vytvořili platby s typem 'postpaid'
  const postpaidUserIds = [];

  for (const [orderType, orders] of Object.entries(orderData)) {
    postpaidUserIds.push(orders.filter((order) => order.type === 'postpaid'));
  }
  console.log(postpaidUserIds);
  

Jak vypadá postPaidUserIds? Napovím vám, že je to nehezké!

  [
    [],
    [
      { userId: 3, date: '2021-08-08', amount: 30, type: 'postpaid' },
      { userId: 4, date: '2021-06-06', amount: 330, type: 'postpaid' }
    ]
  ]
  

Pokud jste rozumní, nechcete psát vlastní logiku pro extrahování objektů a jejich rozložení do jednoho pole. Stačí použít metodu 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() zploští jen jednu úroveň. Pokud jsou objekty zanořené hlouběji, flatten() nepomůže. V takovém případě použijte flattenDeep(). Ale pamatujte, že použití flattenDeep() na velké struktury může být pomalé (funguje na principu rekurze).

Je objekt/pole prázdné?

Kvůli tomu, jak fungují „falešné“ hodnoty v JavaScriptu, může být kontrola prázdnoty matoucí.

Jak zjistíte, zda je pole prázdné? Můžete zkontrolovat jeho délku. A jak zkontrolujete, zda je objekt prázdný? No… To už je problém. Začnou se vám v hlavě míhat příklady jako [] == false a {} == false. Potřebujete jednoduché řešení, které vám nezpůsobí problémy při testování.

Práce s chybějícími daty

V reálném světě data nejsou vždy perfektní. Typickým příkladem jsou chybějící objekty nebo null hodnoty v odpovědi API.

Předpokládejme, že jsme z API dostali následující objekt:

  const apiResponse = {
    id: 33467,
    paymentRefernce: 'AEE3356T68',
    // Objekt `order` chybí
    processedAt: `2021-10-10 00:00:00`,
  };
  

Obecně dostáváme v odpovědi objekt objednávky, ale nemusí tomu tak být vždy. Co když náš kód s tímto objektem počítá? Jednou z možností je psát defenzivní kód. Ale pokud je objekt zanořený, brzy byste psali velmi ošklivý kód:

  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
    );
  }
  

Takové řešení je těžké psát, číst i udržovat. Naštěstí má Lodash jednoduchý způsob, jak se s tím vypořádat.

  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
  

Můžete také nastavit výchozí hodnotu pro chybějící hodnoty:

  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() je úžasná funkce. Je jednoduchá, a přitom šetří spoustu problémů! 😇

Odbavení (Debouncing)

Debouncing je běžné téma ve vývoji frontendu. Myšlenka je taková, že někdy je užitečné akci nespouštět okamžitě, ale po nějaké době.

Představte si e-shop s vyhledávacím polem. Nechceme, aby uživatel musel stisknout „Enter“, aby se mu zobrazily návrhy. Pokud přidáme listener události k onChange() pro vyhledávací pole a spustíme API call pro každý stisk klávesy, zadělali bychom našemu backendu na problémy. Bylo by mnoho zbytečných požadavků (např. pokud se vyhledává „bílý koberec“, bude odesláno 18 požadavků!).

Řešením je debouncing: neodesíláme API call, jakmile se změní text, ale počkáme (např. 200ms). Pokud během této doby dojde k dalšímu stisku klávesy, zrušíme předchozí časovač a začneme čekat znovu. Výsledkem je, že požadavek odešleme, až když uživatel přestane psát.

Samotný proces debouncingu je složitý, ale velmi jednoduchý s Lodashem.

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

  // API s psími plemeny
  const fetchDogBreeds = () =>
    axios
      .get('https://dog.ceo/api/breeds/list/all')
      .then((res) => console.log(res.data));

  const debouncedFetchDogBreeds = _.debounce(fetchDogBreeds, 1000); // po jedné sekundě
  debouncedFetchDogBreeds(); // data se zobrazí až po nějaké době
  

Pokud si myslíte, že setTimeout() by udělal stejnou práci, není to tak jednoduché. Lodashův debounce má mnoho dalších funkcí. Například můžete zajistit, že debouncing nebude trvat neomezeně dlouho. I když dojde ke stisku klávesy pokaždé, když se funkce má spustit, můžete zajistit, že API call bude odeslán, i tak, po dvou sekundách. K tomu slouží možnost maxWait:

  const debouncedFetchDogBreeds = _.debounce(fetchDogBreeds, 150, { maxWait: 2000 }); // debouncing pro 250ms, ale API call se odešle i tak po 2 sekundách
  

Přečtěte si oficiální dokumentaci, kde najdete více detailů!

Odstranění hodnot z pole

Nechce se mi psát kód pro odstraňování položek z pole. Musím získat index položky, zkontrolovat, jestli je index platný, zavolat metodu splice() atd. Nikdy si nepamatuji syntaxi a nakonec se mi vždy vloudí nějaká chyba.

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

Všimněte si dvou věcí:

  • Původní pole se změnilo.
  • Metoda pull() odstraní všechny instance, včetně duplikátů.

Existuje i metoda pullAll(), která jako druhý parametr přijímá pole, což usnadňuje odstraňování více položek najednou. Sice bychom mohli použít pull() s operátorem spread, ale Lodash vznikl v době, kdy operátor spread nebyl součástí jazyka!

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

Poslední index prvku

Nativní metoda indexOf() v JavaScriptu je skvělá, až na to, že nemůžete procházet polem odzadu. Můžete sice napsat dekrementační smyčku, ale proč nepoužít elegantnější techniku?

Zde je rychlé řešení s metodou lastIndexOf() z Lodashe:

  const integers = [2, 4, 1, 6, -1, 10, 3, -1, 7];
  const index = _.lastIndexOf(integers, -1);
  console.log(index); // 7
  

Bohužel neexistuje varianta této metody, která by umožnila hledat složité objekty nebo předat vlastní funkci pro hledání.

zip. Rozepnout!

Pokud nejste vývojář v Pythonu, zip/unzip je nástroj, který si v kariéře JavaScript vývojáře pravděpodobně nikdy nevšimnete. Nicméně je to jedna z nejlepších a málo známých utilit, která vám může pomoci psát stručný kód.

Na rozdíl od toho, jak to zní, zip/unzip nemá nic společného s kompresí. Je to spíš seskupovací operace, kde se pole stejné délky převedou na jediné pole polí, kde prvky na stejné pozici jsou seskupeny dohromady (zip()) a zpět (unzip()). Začíná to být trochu mlhavé, tak se podívejte na kód:

  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' ] ]
  

Tři původní pole byla převedena na jedno pole o dvou prvcích, kde každý prvek představuje jedno zvíře i s vlastnostmi. Index 0 říká, o jaký druh zvířete se jedná, index 1 je jeho velikost a index 2 je jeho váha. S daty se tak nyní lépe pracuje. Jakmile s daty provedete potřebné operace, můžete je znovu rozdělit pomocí metody unzip():

  const animalData = _.unzip(groupedAnimals);
  console.log(animalData);
  // [ [ 'duck', 'sheep' ], [ 'small', 'large' ], [ 'less', 'more' ] ]
  

Nástroj zip/unzip vám sice nezmění život ze dne na den, ale jednoho dne ho určitě oceníte!

Závěr 👨‍🏫

(Veškerý zdrojový kód použitý v tomto článku najdete zde. Můžete si ho vyzkoušet přímo v prohlížeči!)

Dokumentace Lodashe je plná příkladů a funkcí, které vás nadchnou. Lodash je jako závan čerstvého vzduchu. Vřele vám doporučuji používat tuto knihovnu ve vašich projektech!