Programuj efektywnie! Zwiększ swoją szybkość, produktywność i satysfakcję z kodowania w JavaScript, poznając kluczowe i często używane funkcje tego języka.
JavaScript jest wszechobecny, od backendu, przez frontend, aż po systemy sterowania statków kosmicznych. Jest to język niezwykle elastyczny, umożliwiający korzystanie zarówno z zaawansowanych wzorców programowania funkcyjnego, jak i klasycznego podejścia obiektowego. Jego składnia, zbliżona do języków z rodziny C, ułatwia programistom przejście z innych środowisk.
Jeśli pragniesz podnieść swoje umiejętności JS, gorąco polecam zaznajomienie się, praktykę i ostateczne opanowanie poniższych kluczowych funkcji. Nie wszystkie są absolutnie niezbędne do rozwiązywania problemów, jednak w wielu sytuacjach mogą znacząco ułatwić pracę, redukując przy tym objętość kodu.
map()
Niemożliwością byłoby stworzenie artykułu o ważnych funkcjach JavaScript, nie wspominając o map()! 😉 Razem z filter() i reduce(), map() tworzy fundamentalne trio. Są to funkcje, które będziesz wykorzystywać często, dlatego warto poświęcić im szczególną uwagę. Zacznijmy od map().
map() często sprawia trudności osobom uczącym się JavaScript. Nie wynika to z jego złożoności, a z faktu, że sposób jego działania wywodzi się z paradygmatu programowania funkcyjnego. Ponieważ w edukacji i branży dominują języki obiektowe, funkcjonalne podejście może wydawać się początkowo obce i nielogiczne.
JavaScript, mimo że jego nowoczesne wersje starają się to zamaskować, jest bardziej funkcyjny niż obiektowy. To jednak temat na inną dyskusję. 🤣 Skupmy się na map().
map() to bardzo prosta funkcja, która operuje na tablicy, transformując każdy jej element w coś nowego i zwracając nową tablicę. Sposób transformacji jest definiowany przez inną funkcję, często anonimową.
To wszystko! Opanowanie składni może zająć chwilę, ale w istocie tak działa funkcja map(). Po co jej używać? To zależy od celu. Załóżmy, że codziennie notowaliśmy temperaturę i zapisaliśmy wyniki w prostej tablicy. Teraz okazuje się, że pomiary były niedokładne i zaniżone o 1,5 stopnia.
Możemy skorygować te dane, wykorzystując map() w następujący sposób:
const weeklyReadings = [20, 22, 20.5, 19, 21, 21.5, 23]; const correctedWeeklyReadings = weeklyReadings.map(reading => reading + 1.5); console.log(correctedWeeklyReadings); // wynik: [ 21.5, 23.5, 22, 20.5, 22.5, 23, 24.5 ]
Innym przykładem, często spotykanym w React, jest tworzenie list elementów DOM na podstawie tablic. Powszechne jest wtedy coś takiego:
export default ({ products }) => { return products.map(product => { return ( <div className="product" key={product.id}> <div className="p-name">{product.name}</div> <div className="p-desc">{product.description}</div> </div> ); }); };
Ten komponent React otrzymuje listę produktów i na jej podstawie generuje listę elementów HTML, transformując każdy obiekt produktu na element div. Oryginalna tablica produktów pozostaje nietknięta.
Można stwierdzić, że map() to nic innego jak „ulepszona” pętla for. Jest to prawda, ale ten argument wypływa z myślenia obiektowego. Funkcje takie jak map() i całe podejście, z którego pochodzą, kładą nacisk na jednolitość, zwięzłość i elegancję kodu.
filter()
filter() to kolejna bardzo użyteczna funkcja. Jak sama nazwa wskazuje, służy do filtrowania tablicy na podstawie zadanych kryteriów i zwracania nowej tablicy, która zawiera tylko elementy spełniające te kryteria.
Wróćmy do naszego przykładu z temperaturami. Załóżmy, że mamy tablicę z maksymalnymi temperaturami każdego dnia minionego tygodnia i chcemy sprawdzić, ile dni było chłodnych. Powiedzmy, że za „chłodny” dzień uznamy taki, w którym temperatura była niższa niż 20 stopni. Możemy to osiągnąć, używając funkcji filter():
const weeklyReadings = [20, 22, 20.5, 19, 21, 21.5, 23]; const colderDays = weeklyReadings.filter(dayTemperature => { return dayTemperature < 20; }); console.log("Liczba chłodniejszych dni w tygodniu: " + colderDays.length); // 1
Anonimowa funkcja przekazywana do filter() musi zwracać wartość logiczną (true lub false). Na jej podstawie filter() decyduje, czy dany element ma zostać uwzględniony w filtrowanej tablicy. Wewnątrz tej funkcji możemy umieścić dowolną logikę, np. wywołania API czy odczyt danych od użytkownika, o ile ostatecznie zwracamy wartość boolean.
Ostrzeżenie: na podstawie mojego doświadczenia jako programisty JavaScript, czuję się w obowiązku zwrócić uwagę na pewien częsty błąd. Wielu programistów, czy to z powodu niechlujstwa, czy słabych podstaw, popełnia subtelne błędy podczas używania filter(). Spójrzmy na zmodyfikowany kod z błędem:
const weeklyReadings = [20, 22, 20.5, 19, 21, 21.5, 23]; const colderDays = weeklyReadings.filter(dayTemperature => { return dayTemperature < 20; }); if(colderDays) { console.log("Tak, w zeszłym tygodniu były chłodniejsze dni."); } else { console.log("Nie, w zeszłym tygodniu nie było chłodniejszych dni."); }
Czy dostrzegasz problem? Jeśli tak, to świetnie! Warunek if na końcu sprawdza wartość colderDays, która w rzeczywistości jest tablicą! Wiele osób popełnia ten błąd, pracując pod presją czasu lub w złym nastroju. Problem z tym warunkiem polega na tym, że JavaScript jest językiem dziwnym i niespójnym, a „prawdziwość” wartości jest jedną z tych kwestii. Choć [] == true zwraca false, co sugeruje, że kod jest poprawny, w warunku if [] jest traktowane jako true! Innymi słowy, kod nigdy nie powie, że w zeszłym tygodniu nie było chłodniejszych dni.
Naprawa jest bardzo prosta, jak w kodzie prezentowanym wcześniej. Sprawdzamy colderDays.length, co daje nam liczbę całkowitą (zero lub więcej), dzięki czemu działa to konsekwentnie w porównaniach logicznych. Zauważ, że filter() zawsze zwraca tablicę, pustą lub nie, więc możemy na tym polegać i pisać pewny kod.
To był dłuższy wywód, niż planowałem, ale błędy tego typu są warte omówienia. Mam nadzieję, że unikniesz dzięki temu setek godzin debugowania! 🙂
reduce()
Spośród wszystkich funkcji omawianych w tym artykule, a także w standardowej bibliotece JavaScript, reduce() zasługuje na miano „zagmatwanej i dziwnej”. Choć jest to funkcja bardzo ważna i często prowadzi do eleganckiego kodu, wielu programistów JavaScript jej unika, preferując bardziej rozbudowane rozwiązania.
Powodem jest to, że funkcja reduce() jest trudna do zrozumienia, zarówno w teorii, jak i w praktyce. Czytając jej opis, trzeba to zrobić kilka razy i nadal mamy wątpliwości, czy dobrze to zrozumieliśmy; a kiedy widzimy ją w akcji, nasz mózg zaczyna przypominać węzeł gordyjski. 🤭
Nie bój się. reduce() nie jest tak skomplikowana jak, na przykład, drzewa B+. Po prostu tego rodzaju logika nie jest często spotykana w codziennej pracy programisty.
Tak więc, po straszeniu Cię, a następnie uspokajaniu, chcę w końcu wyjaśnić, czym jest ta funkcja i kiedy może się przydać.
Jak nazwa wskazuje, reduce() służy do redukcji czegoś. Redukuje tablicę do pojedynczej wartości (liczby, stringa, funkcji, obiektu, czegokolwiek). Innymi słowy, przekształca tablicę w pojedynczą wartość. Ważne jest, że wartością zwracaną nie jest tablica, jak w przypadku map() i filter(). Zrozumienie tego to już połowa sukcesu. 🙂
Aby móc zredukować tablicę, musimy dostarczyć odpowiednią logikę. Z Twojego doświadczenia jako programisty JS wiesz już, że robimy to za pomocą funkcji, którą nazywamy funkcją redukującą. Funkcja ta jest pierwszym argumentem funkcji reduce(). Drugim argumentem jest wartość początkowa (np. liczba, string). (Za chwilę wyjaśnię, czym jest ta „wartość początkowa”).
Zatem, ogólnie, wywołanie reduce() wygląda tak: array.reduce(reducerFunction, startValue). Przejdźmy teraz do sedna: funkcja redukująca. Mówi ona funkcji reduce(), jak przekształcić tablicę w pojedynczą wartość. Przyjmuje dwa argumenty: zmienną, która działa jak akumulator (wyjaśnię to) i zmienną przechowującą bieżącą wartość.
Wiem, wiem… To sporo terminologii jak na jedną funkcję, która nie jest nawet obowiązkowa w JavaScript. 😝😝 Dlatego wiele osób unika reduce(). Jednak nauka krok po kroku pozwoli Ci zrozumieć i docenić jej wartość.
Wracając do tematu, „wartość początkowa” przekazywana do reduce() to… no cóż, wartość początkowa do obliczeń. Na przykład, jeśli zamierzamy używać mnożenia, wartość początkowa 1 ma sens; dla dodawania możemy zacząć od 0, itd.
Spójrzmy teraz na sygnaturę funkcji redukującej. Funkcja redukująca ma postać: reducerFunction(accumulator, currentValue). „Akumulator” to zmienna, która zbiera i przechowuje wynik obliczeń. Działa to podobnie jak zmienna o nazwie total, używana do sumowania wszystkich elementów tablicy za pomocą czegoś w stylu total += arr[i]. W reduce() akumulator jest początkowo ustawiany na podaną wartość początkową, następnie odwiedzane są kolejne elementy tablicy, wykonywane są obliczenia, a wynik zapisywany w akumulatorze, i tak dalej…
Czym jest „wartość bieżąca” w funkcji redukującej? Jest to wartość zmiennej reprezentującej aktualnie przetwarzany element tablicy. Wyobraź sobie, że przechodzisz przez tablicę, zaczynając od indeksu zero, i zatrzymujesz się na każdym elemencie. „Bieżąca wartość” to właśnie ten element (można to porównać do pętli for, jeśli to pomaga).
Po tej długiej dyskusji zobaczmy prosty przykład, aby zobaczyć, jak to wszystko łączy się w praktyce. Załóżmy, że mamy tablicę zawierającą pierwsze n liczb naturalnych (1, 2, 3… n) i chcemy obliczyć silnię liczby n. Wiemy, że aby to zrobić, musimy po prostu pomnożyć wszystkie liczby, co prowadzi nas do takiego kodu:
const numbers = [1, 2, 3, 4, 5]; const factorial = numbers.reduce((acc, item) => acc * item, 1); console.log(factorial); // 120
W tych trzech liniach kodu dzieje się sporo, więc przeanalizujmy to krok po kroku w kontekście naszej dyskusji. Liczby to tablica z wartościami, które chcemy pomnożyć. Następnie, w wywołaniu numbers.reduce(), widzimy, że początkową wartością dla acc ma być 1 (ponieważ nie wpływa to na wynik mnożenia). Funkcja redukująca `(acc, item) => acc * item` mówi, że wartością zwracaną dla każdej iteracji ma być element pomnożony przez to, co już jest w akumulatorze. Iteracja i przechowywanie wyniku mnożenia w akumulatorze dzieje się „za kulisami” i to jest główny powód, dla którego reduce() jest tak trudna do zrozumienia dla wielu programistów.
Po co używać reduce()?
To dobre pytanie i nie ma na nie jednoznacznej odpowiedzi. Wszystko, co robi reduce(), można osiągnąć za pomocą pętli for, forEach() itp. Jednak te techniki często skutkują większą ilością kodu, który jest trudniejszy do odczytania, szczególnie pod presją czasu. Dodatkowo pojawia się kwestia niezmienności: dzięki reduce() i podobnym funkcjom mamy pewność, że oryginalne dane nie zostały zmodyfikowane. To samo w sobie eliminuje wiele klas błędów, szczególnie w aplikacjach rozproszonych.
Co więcej, reduce() jest bardziej elastyczna. Akumulatorem może być obiekt, tablica, a nawet funkcja. To samo dotyczy wartości początkowej i innych elementów wywołania. Istnieje więc spora swoboda w projektowaniu kodu wielokrotnego użytku.
Jeśli nadal nie jesteś przekonany, to w porządku. Społeczność JavaScript jest podzielona co do „zwięzłości”, „elegancji” i „mocy” reduce(). Nie ma nic złego w jej nieużywaniu. 🙂 Ale obejrzyj kilka przykładów, zanim podejmiesz ostateczną decyzję.
some()
Załóżmy, że masz tablicę obiektów, z których każdy reprezentuje osobę. Chcesz sprawdzić, czy w tablicy jest co najmniej jedna osoba w wieku powyżej 35 lat. Nie interesuje Cię, ile jest takich osób, ani ich lista. Szukasz więc czegoś, co jest odpowiednikiem „jednego lub więcej”.
Jak to zrobić?
Możesz użyć zmiennej flagi i pętli for, aby to osiągnąć:
const persons = [ { name: 'Osoba 1', age: 32 }, { name: 'Osoba 2', age: 40 }, ]; let foundOver35 = false; for (let i = 0; i < persons.length; i ++) { if(persons[i].age > 35) { foundOver35 = true; break; } } if(foundOver35) { console.log("Tak, jest tu kilka osób!"); }
Problem? Kod jest zbyt „C-podobny” lub „Java-podobny”. Doświadczony programista JS może pomyśleć o „brzydkim” lub „okropnym” rozwiązaniu. 😝 I miałby rację. Jednym ze sposobów na ulepszenie tego kodu jest użycie np. map(), ale nawet wtedy rozwiązanie nie będzie idealne.
Okazuje się, że mamy wbudowaną funkcję o nazwie some(), która robi dokładnie to, czego potrzebujemy. Działa ona na tablicach i akceptuje funkcję „filtrującą”, która zwraca wartość logiczną true lub false. W praktyce robi ona to, co chcieliśmy osiągnąć w poprzednim przykładzie, ale w zwięzły i elegancki sposób. Oto, jak możemy jej użyć:
const persons = [ { name: 'Osoba 1', age: 32 }, { name: 'Osoba 2', age: 40 }, ]; if(persons.some(person => { return person.age > 35 })) { console.log("Znaleziono kilka osób!"); }
To samo wejście, ten sam wynik, ale spójrz, jak bardzo zredukowaliśmy ilość kodu! Zauważ też, jak zmniejsza się obciążenie poznawcze, bo nie musimy już analizować kodu linia po linii, jakbyśmy sami byli kompilatorem! Kod czyta się teraz prawie jak język naturalny.
every()
Podobnie do some(), mamy też funkcję every(). Zgadłeś, że ona również zwraca wartość logiczną, w zależności od tego, czy wszystkie elementy tablicy przejdą dany test. Test dostarczamy jako funkcję anonimową. Oszczędzę Ci widoku naiwnej wersji kodu, więc od razu pokażę, jak używać every():
const entries = [ { id: 1 }, { id: 2 }, { id: 3 }, ]; if(entries.every(entry => { return Number.isInteger(entry.id) && entry.id > 0; })) { console.log("Wszystkie wpisy mają prawidłowe id."); }
Kod sprawdza, czy wszystkie obiekty w tablicy mają prawidłową właściwość id. Definicja „prawidłowości” zależy od kontekstu, ale w tym przypadku założyłem, że id to nieujemna liczba całkowita. Ponownie, widzimy, jak prosty i elegancki jest ten kod, co jest celem tej i podobnych funkcji.
includes()
Jak sprawdzić, czy w ciągu znaków lub tablicy znajduje się podciąg/element? Jeśli jesteś jak ja, to szybko sięgasz po indexOf(), a następnie szukasz w dokumentacji, co ta metoda zwraca. Nie jest to wygodne, a zwracane wartości ciężko zapamiętać (szybko, co oznacza proces zwracający 2 systemowi operacyjnemu?).
Jest jednak fajna alternatywa: includes(). Użycie jest tak proste, jak nazwa, a kod jest bardzo przejrzysty. Pamiętaj, że dopasowanie w includes() uwzględnia wielkość liter, ale to intuicyjne.
const numbers = [1, 2, 3, 4, 5]; console.log(numbers.includes(4)); const name = "Ankush"; console.log(name.includes('ank')); // false, bo pierwsza litera jest mała console.log(name.includes('Ank')); // true, zgodnie z oczekiwaniem
Nie oczekuj jednak za wiele od tej metody:
const user = {a: 10, b: 20}; console.log(user.includes('a')); // błąd, obiekty nie mają metody "includes"
Nie może zaglądać do obiektów, bo nie jest dla nich zdefiniowana. Ale wiemy, że działa na tablicach, więc może… 🤔
const persons = [{name: 'Phil'}, {name: 'Jane'}]; persons.includes({name: 'Phil'});
Co się stanie po uruchomieniu tego kodu? Nie wybuchnie, ale wynik jest rozczarowujący: false. 😫😫 Ma to związek z obiektami, wskaźnikami i tym, jak JavaScript widzi i zarządza pamięcią. To jednak temat na osobną dyskusję (możesz zacząć tutaj), na tym poprzestanę.
Możemy sprawić, by kod działał poprawnie, jeśli go zmodyfikujemy, ale robi się to trochę… dziwne:
const phil = {name: 'Phil'}; const persons = [phil, {name: 'Jane'}]; persons.includes(phil); // true
Pokazuje to jednak, że możemy użyć includes() z obiektami, więc nie jest to całkowita klapa. 😄
slice()
Załóżmy, że masz ciąg znaków i chcesz pobrać jego fragment, który zaczyna się na literę „r” i kończy na „z” (konkretne znaki nie są ważne). Jak byś do tego podszedł? Prawdopodobnie utworzyłbyś nowy string i przechowywał w nim odpowiednie znaki. Albo, jak większość programistów, użyłbyś dwóch indeksów: początkowego i końcowego.
Oba podejścia są dobre, ale istnieje koncepcja „krojenia”, która oferuje wygodne rozwiązanie. Nie ma w tym nic skomplikowanego – krojenie oznacza tworzenie mniejszego ciągu/tablicy z podanego, tak jak kroimy owoce. Spójrzmy na przykład:
const headline = "A dzisiejszym odcinku specjalnym, naszym gościem jest ten, na którego wszyscy czekali!"; const startIndex = headline.indexOf('gościem'); const endIndex = headline.indexOf('czekali'); const newHeadline = headline.slice(startIndex, endIndex); console.log(newHeadline); // gościem jest ten, na którego wszyscy
Używając slice(), podajemy dwa indeksy – jeden, w którym chcemy zacząć kroić, a drugi, w którym chcemy skończyć. Indeks końcowy nie jest uwzględniany w wyniku. Dlatego w powyższym przykładzie brakuje słowa „czekali”.
Krojenie jest bardziej popularne w innych językach, szczególnie w Pythonie. Programiści Pythona nie wyobrażają sobie życia bez tej funkcjonalności, ponieważ język zapewnia bardzo zwięzłą składnię do krojenia.
slice() jest eleganckie, wygodne i nie ma powodu, by z niego nie korzystać. Nie jest to też „lukier składniowy”, który negatywnie wpływa na wydajność, bo tworzy płytkie kopie. Polecam programistom JavaScript zapoznanie się z slice() i dodanie go do swojego arsenału!
splice()
splice() brzmi jak kuzyn slice() i w pewnym sensie tak jest. Obie funkcje tworzą nowe tablice/ciągi na podstawie oryginalnych, z tą małą, ale ważną różnicą: splice() usuwa, zmienia lub dodaje elementy, ale **modyfikuje oryginalną tablicę**. To „zniszczenie” oryginalnej tablicy może powodować problemy, jeśli nie będziemy ostrożni. Zastanawiam się, dlaczego twórcy nie zastosowali podejścia znanego ze slice() i pozostawili oryginalną tablicę bez zmian. Pamiętajmy jednak, że język powstał w zaledwie 10 dni.
Pomimo moich narzekań, spójrzmy, jak działa splice(). Pokażę przykład usuwania elementów, bo to jest najczęstsze zastosowanie tej metody. Nie będę pokazywać dodawania i wstawiania, bo to proste.
const items = ['jajka', 'mleko', 'ser', 'chleb', 'masło']; items.splice(2, 1); console.log(items); // [ 'jajka', 'mleko', 'chleb', 'masło' ]
Powyższe wywołanie splice() mówi: zacznij od indeksu 2 (trzecie miejsce) i usuń jeden element. W tablicy „ser” jest trzecim elementem, więc zostaje usunięty, a tablica jest skrócona. Usunięte elementy są zwracane przez splice() w formie tablicy, więc możemy „przechwycić” „ser” do zmiennej, gdybyśmy chcieli.
Z mojego doświadczenia wynika, że indexOf() i splice() świetnie ze sobą współpracują – szukamy indeksu elementu, a następnie go usuwamy. Należy jednak pamiętać, że nie zawsze jest to najwydajniejsza metoda, często użycie kluczy obiektowych (mapa skrótów) jest znacznie szybsze.
shift()
shift() to wygodna metoda, która służy do usuwania pierwszego elementu tablicy. Można to zrobić za pomocą splice(), ale shift() jest łatwiejszy do zapamiętania i intuicyjny, gdy chcemy usunąć tylko pierwszy element.
const items = ['jajka', 'mleko', 'ser', 'chleb', 'masło']; items.shift() console.log(items); // [ 'mleko', 'ser', 'chleb', 'masło' ]
unshift()
Podobnie jak shift() usuwa pierwszy element, unshift() dodaje element na początku tablicy. Jego użycie jest proste:
const items = ['jajka', 'mleko']; items.unshift('chleb') console.log(items); // [ 'chleb', 'jajka', 'mleko' ]
Muszę jednak ostrzec: w przeciwieństwie do popularnych push() i pop(), shift() i unshift() są wyjątkowo nieefektywne (ze względu na sposób działania algorytmów). Jeśli działasz na dużych tablicach (powyżej 2000 elementów), zbyt wiele wywołań tych funkcji może spowolnić aplikację.
fill()
Czasami trzeba zmienić kilka elementów na jedną wartość, lub nawet „zresetować” całą tablicę. W takich sytuacjach fill() uchroni nas przed pętlami i błędami „off-by-one”. Można go użyć, aby zastąpić część lub całą tablicę daną wartością. Zobaczmy przykłady:
const heights = [1, 2, 4, 5, 6, 7, 1, 1]; heights.fill(0); console.log(heights); // [0, 0, 0, 0, 0, 0, 0, 0] const heights2 = [1, 2, 4, 5, 6, 7, 1, 1]; heights2.fill(0, 4); console.log(heights2); // [1, 2, 4, 5, 0, 0, 0, 0]
Inne funkcje warte wspomnienia
Powyższa lista obejmuje funkcje, z którymi większość programistów JavaScript spotyka się w swojej karierze, ale nie jest to lista kompletna. Istnieje wiele mniejszych, ale przydatnych funkcji (metod), których nie da się opisać w jednym artykule. Kilka przykładów:
- reverse()
- sort()
- entries()
- fill()
- find()
- flat()
Zachęcam Cię do przejrzenia ich dokumentacji, abyś miał świadomość, że takie udogodnienia istnieją.
Podsumowanie
JavaScript to duży język, pomimo niewielkiej liczby podstawowych konceptów. Wiele dostępnych nam funkcji (metod) stanowi dużą część jego rozmiaru. Ponieważ JavaScript dla wielu jest językiem drugorzędnym, nie zagłębiamy się wystarczająco, tracąc wiele pięknych i przydatnych funkcji. To samo dotyczy paradygmatu programowania funkcyjnego, ale to temat na inny artykuł! 😅
Poświęć czas na naukę podstaw języka (i, jeśli to możliwe, popularnych bibliotek narzędziowych, takich jak Lodash). Nawet kilka minut poświęconych na ten wysiłek zaowocuje znacznym wzrostem produktywności oraz znacznie czystszym i zwięzłym kodem.
newsblog.pl
Maciej – redaktor, pasjonat technologii i samozwańczy pogromca błędów w systemie Windows. Zna Linuxa lepiej niż własną lodówkę, a kawa to jego główne źródło zasilania. Pisze, testuje, naprawia – i czasem nawet wyłącza i włącza ponownie. W wolnych chwilach udaje, że odpoczywa, ale i tak kończy z laptopem na kolanach.