Jak działają map, filter i reduce w JavaScript?
Trudno wyobrazić sobie programowanie funkcyjne bez metod map, filter i reduce, a niewłaściwe zrozumienie ich działania może doprowadzić do trudnych w wykryciu błędów. Jest to o tyle istotne, że równie trudno wyobrazić sobie współczesny front-end bez kwestii programowania funkcyjnego. Dzisiaj wyjaśnię jak one działają oraz kiedy powinniśmy ich używać.
Spis treści
1. Funkcja .map()
Funkcja .map()
jest jedną z najczęściej używanych metod programowania funkcyjnego. Wykonuje się ją na tablicy, transformując po kolei każdy z jej elementów, następnie zwracając nową tablicę ze zmodyfikowaną zawartością.
Istotną właściwością zarówno metody map(), jak również filter() i reduce() jest fakt, że tworzą one nową tablicę ze zmienioną zawartością, zamiast modyfikowania tej przekazanej wcześniej. Wpisuje się to w zasadę niezmienności (immutability), jedną bardzo przydatnych i pożądanych często właściwości. Przykładem zastosowania niezmienności w praktyce jest stan aplikacji opartej o Redux. Domyślnie jest on tylko do odczytu, a każda aktualizacja zastępuje całe drzewo stanu nową, zaktualizowaną wersją.
Poniżej prosty przykład wykorzystania .map():
const array1 = [1, 2, 3]; const array2 = array.map(el => el * 2);
Pomimo, że powyższy kod jest bardzo prosty, to zdążyło się tam zadziać kilka ciekawych rzeczy.
- Funkcja .map() iteruje po każdym elemencie tablicy, na której została wykonana i przekazując jego wartość jako argument funkcji typu callback. Wewnątrz callbacka możemy wykonać dowolną czynność i zwrócić dowolną wartość.
- Tablica array nie została zmodyfikowana i dalej zawiera wartości [1, 2, 3].
- Do stałej array2 została przypisana całkowicie nowa tablica zwrócona przez funkcję .map(), ze zmodyfikowaną zawartością pierwszej tablicy array1, czyli [2, 4, 6].
2. Funkcja .filter()
Funkcja .filter()
podobnie do poprzednio omawianej .map()
nie modyfikuje tablicy, na której została wykonana. Różni się natomiast działaniem i zastosowaniem.
Sprawdza ona, czy elementy tablicy, po której iteruje spełniają określony warunek. Jeżeli tak, zostają dodane do nowo zwracanej tablicy, jeżeli nie – zostają pomijane. Nota bene sama nazwa wskazuje na “przepuszczanie” tylko tych wartości, które spełniają określone kryteria.
Przykład funkcji .filter()
const array1 = [1, 2, 3, 4, 5]; const array2 = array.filter(el => el > 2);
Ponownie widzimy prosty przykład, w którym dzieje się wiele ciekawych rzeczy,
- Ponownie tablica array1 pozostaje niezmieniona.
- Tablica array2 zawiera wartości tych elementów, które spełniły kryterium “> 2”, czyli [3, 4, 5].
- Filter również przyjmuje funkcję callback, natomiast jej działanie różni się od .map(). W tym przypadku musi ona zwracać wartość typu boolean. Jeżeli zwraca prawdę, element którego dotyczy zostanie przekazany do nowej tablicy, jeżeli zwraca fałsz, element zostanie pominięty tak, jak zostały pominięte dwie pierwsze liczby w tablicy array1.
3. Funkcja .reduce()
Z doświadczenia wiem, że funkcja .reduce()
sprawia najwięcej trudności, a jej działanie jest bardziej skomplikowane. Jej niewłaściwe użycie jest powodem wielu błędów i może skutkować innym finalnym kształtem przetwarzanych danych, niż byśmy sobie tego życzyli.
Funkcji .reduce()
używamy na przykład, kiedy chcemy zmienić tablicę w pojedynczą wartość. Takim sposobem możemy zsumować wartości liczbowe, które ona zawiera lub połączyć znaki (typu string) w jeden wyraz. Zastosowań jest rzecz jasna o wiele więcej, przeanalizujemy jednak tę metodę na najprostszym przykładzie.
Zajmiemy się tym drugim przykładem.
const array = [‘J’, ‘U’, ‘S’, ‘T’, ‘J’, ‘O’, ‘I’, ‘N’, ‘.’, ‘I’, ‘T’]; const newText = array.reduce((prev, next) => `${prev}${next}`);
- array dalej zawiera nie zmienioną tablicę
- newText zawiera wartość tekstową “JUSTJOIN.IT”
- Możemy alternatywnie zapisać zwracaną wartość callbacku jako prev+next, ponieważ plus pomiędzy zmiennymi typu string łączy je
Callback funkcji .reduce() przyjmuje cztery argumenty:
- previousValue – ostatnio zwróconą wartość callbacka (mamy też możliwość ustawienia wartości początkowej)
- currentValue – czyli obecnie przetwarzany element tablicy
- index (opcjonalny) – czyli kolejność przetwarzanego elementu
- array (opcjonalny) – oraz tablicę na której wykonujemy reduce
Dodatkowo mamy możliwość przekazania początkowej wartości argumentu previousValue, a w przypadku podanego przykładu, prezentowałoby się to tak:
const newText = array.reduce((prev, next) => `${prev}${next}`, ‘WE ARE ’);
Co da efekt w postaci “WE ARE JUSTJOIN.IT” :)’.
Podsumowanie
Powyższe metody pozwalają niesamowicie zmienić czytelność kodu, usprawnić przetwarzanie danych i zwiększyć przyjemność z tworzenia oprogramowania. Choć w szybkości działania ustępują nieco klasycznej i znanej pętli for(), to moim zdaniem zdecydowanie warto je stosować, ze względu na szereg zalet.
Zdjęcie główne artykułu pochodzi z unsplash.com.