Trzy lukrowane pączki leżące na blacie pełnym kolorowej posypki.

Spread, rest i default – Powtórka przed ReactJS #4

Funkcje wykorzystujące tablice to chleb powszedni każdego programisty JavaScript. Mając to na uwadze, komitet TC39 zatroszczył się o wprowadzenie kilku użytecznych usprawnień, które rozszerzają nasze możliwości. Oprócz nowinek, zastąpimy stare rozwiązania bardziej eleganckim kodem. Panie i Panowie, oto bohaterowie dzisiejszego wpisu: spread, rest i default.

Jako że tematyka jest stosunkowo prosta i intuicyjna, to skoncentruje się na omawianiu konkretnych, praktycznych zastosowań. Osoby czujące niedosyt teorytycznych wywodów (anyone?) odsyłam do materiałów źródłowych. Jeżeli miałeś w przeszłości styczność z tablicami, to na pewno szybko podchwycisz o co się rozchodzi z tym całym …wielokropkowym zamieszaniem.

Co omówię tym razem? Zobaczmy:

  1. Spread operator – jak to działa?
  2. Przykłady zastosowania spread operatora
  3. Parametry rest – jak to działa?
  4. Przykłady zastosowania parametrów rest
  5. Wartości domyślne

Uwaga, nie obyło się bez cotygodniowej niespodzianki. Na końcu wpisu czekają na Was odnośniki do zadań praktycznych ;).

Spread – czym jest ten operator?

Spread operator pozwala rozprowadzić poszczególne elementy obiektu iterowalnego (najczęstszym przedstawicielem tego gatunku są tablice). W przypadku funkcji, mamy taką możliwość, gdy oczekiwane jest zero lub więcej argumentów. To samo tyczy się elementów dla literałów tablicowych. Bardzo możliwe, że wraz z zakończeniem prac nad specyfikacją ES7 (obecnie znajduje się w trzecim etapie), zostanie wprowadzona możliwość wykorzystywania spread dla literałów obiektowych.

Składnia operatatora spread składa się z wielokropka ..., po którym odwołujemy się do identyfikatora lub literału.

Spread, w zależności od kontekstu, zamienia elementy obiektu iterowalnego w argumenty wywoływanej funkcji lub w elementy nowej tablicy.

Przykłady zastosowania spread operatora

Spread operator znajduje szerokie zastosowanie w pracy z funkcjami. Wśród książkowych przykładów króluje Math.max(). Metoda ta przyjmuje dowolną ilość argumentów, ale nie działa z tablicami. W przeszłości była to świetna okazja do napisania własnej funkcji obliczającej największy element tablicy. Teraz możemy wyręczyć się spread operatorem.

Nic nie stoi na przeszkodzie, aby podczas jednego wywołania przekazać do funkcji większą liczbę tablic.

Można również łaczyć taka składnię z użyciem wartości prymitywnych.

Najpopularniejszym (i najszybszym) sposobem na stworzenie kopii tablicy jest wykorzystanie metody Array.prototype.slice. Jeżeli zależy nam na poprawie czytelności, możemy rozważyć zastosowanie spread opearatora.

Warto pamiętać: obiekty wewnątrz tablicy są referencjami, więc nie otrzymujemy kopii ‚per se’.

Spread operator pozwala na proste łączenie tablic. Dzięki temu, może służyć jako czytelniejsza alternatywa dla Array.prototype.concat.

Ten sam efekt złączenia kilku tablic możemy osiągnąć wykorzystując Array.prototype.push wraz z spread operatorem.

Spread operator można stosować w połączeniu z wywołaniem konstruktora.

Możemy wykonać prostą konwersję z zbioru (ang. Set) do tablicy.

Podobnie sprawy mają się w przypadku konwersji NodeList/arguments.

Ten sam efekt można otrzymać dzięki Array.from(). Ze względu na czytelność, zachęcam do wykorzystywania tej metody zamiast spread operatora.

Łącząc ze sobą działanie z konstruktorami i konwersję z zbiorów, możemy łatwo uzyskać tablicę z unikatowymi wartościami.

Skoro spread działa na wszystkich obiektach iterowalnych, to nie zaszkodzi potraktować nim jakiś łańcuch. W ten sposób otrzymamy tablicę wszystkich znaków.

Parametry rest – jak to działa?

Parametry rest pozwalają zebrać nieokreśloną ilość argumentów jako tablicę. Tak samo jak w przypadku spread operatora, używamy w tym celu wielokropka.

Jak widać, mimo takiej samej składni, parametry rest mają odwrotne działanie względem swojego kuzyna, spread operatora. Co decyduje o tym jaki efekt będzie miało zastosowanie wielokropka? Kontekst operacji. Rest w przypadku wywołania funkcji i destrukturyzacji tablicy zostanie użyte do pobrania listy przekazanych argumentów. Spread znajduje zastosowanie przy konstruowaniu tablicy. Do tego, podczas wywołania funkcji, umożliwia wypełnienie jej argumentów elementami tablicy.

Mam nadzieję, że rozróżnienie tych dwóch funkcjonalności nie stanowi już problemu.

Przykłady zastosowania rest operatora?

Najbardziej intuicyjnym zastosowaniem parametrów rest jest zastąpienie obiektu arguments.

Nawet tak prosta deklaracja zyskała na czytelności. Od razu wiadomo, co mamy przekazać do funkcji i co otrzymamy z powrotem. Jak wiele możemy zyskać zobrazuje bardziej skomplikowany przykład autorstwa Nicolása Bevacqua’y.

Otrzymaliśmy ściśle lepszy kod, tzn. zredukowaliśmy długość kodu, zyskując na czytelności. Tak się żyje.

Różnice pomiędzy parametrami rest i obiektem arguments.

  • Parametry rest to tylko te, które nie otrzymały oddzielnej nazwy. Arguments przechowuje wszystkie argumenty przekazane do funkcji.
  • Obiekt arguments nie jest prawdziwą tablicą, parametry rest są instancjami Array. W praktyce pozwala to na bezpośrednie zastosowanie metod pokroju map, forEach itd.

Zastosowanie parametrów rest zamiast obiektu arguments, w wielu przypadkach, pozwala na zredukowanie linijek kodu. Rozważmy jeden z wyjątków.


Jak widać, pozostanie przy arguments daje nam możliwość ustawienia wartości domyślnych dla parametrów. W przypadku rest nie jest to takie proste, ale znajomość destrukturyzacji otwiera nam wiele ścieżek.

Warto przypomnieć, że omawiając funkcje strzałkowe, jako jeden z minusów wskazałem brak dostępu do obiektu arguments. Parametry rest niwelują ten problem.

Co do błędów początkującego, parametry rest zbierają wszystkie pozostałe argumenty, więc następujący zapis nie ma sensu:

Rest możemy wykorzystać podczas destrukturyzacji obiektów i tablic.

Same parametry rest również można poddawać destrukturyzacji.

W tym przykładzie, parametr rest pozwala na zebranie wszystkich parametrów następujących po pierwszym, nazwanym parametrze. Następnie za pomocą funkcji map, możemy przemnożyć wszystkie elementy przez pierwszy parametr.

Parametry rest, aż proszą się o korzystanie z dobrodziejstw programowania funkcyjnego. Jeżeli nie miałeś/aś jeszcze styczności z metodami filter, map i reduce, to bez obaw. Ich działanie przybliżę w jednym z nadchodzących wpisów.

Wartości domyślne

Na deser zostawiłem najprostszą z omawianych dzisiaj funkcjonalności. Zanim do niej przejdziemy, przeanalizujmy czego możemy spodziewać się w starszym kodzie. Praktycznie każdy zna stary, dobry patent z ustawianiem domyślnej wartości parametru za pomocą operatora ||.

Jednak to rozwiązanie ma jedną zasadnicza słabą stronę. Jeżeli umyślnie przekażemy do funkcji wartość, która jest przetwarzana jako ‚falsy’, to wbrew intencjom otrzymamy wartość domyślną.

Możemy przebudować nasz kod z troska o właściwą interpretację ze strony silnika.

Wszystko fajnie, działa jak powinno (chyba, że przewidujemy intencjonalne wykorzystanie wartości undefined…). Tyle, że naprodukowaliśmy dużo dodatkowego kodu, który bardziej koncentruje się na walce z językiem niż rozwiązywaniu problemu.

Po raz kolejny ES6 wyszło na przeciw naszym potrzebom. Zobaczmy jak prezentują się wartości domyślne.

Z powodu składni nie możemy stosować wartości domyślnych w połączeniu z spread/rest. Jak zaprezentowałem w przypadku restów, możemy ratować się destrukturyzacją.

Wartości domyślne mogą być dowolnym wyrażeniem, nawet wywołaniem funkcji.

Należy zachować ostrożność, skomplikowane operacje mogą doprowadzić do nieprzewidzianych komplikacji.

Parametry posiadają swój własny zakres. W związku z tym następuje kilka następujących subtelności: w z wyrażenia w + 1 jest pierw wyszukiwane w zakresie parametrów. Nie zostaje odnalezione, więc silnik przechodzi do zewnętrznego zakresu skąd pobiera wartość zmiennej w. Nastepnie x użyte w wartości domyślnej dla parametru y zostaje odnalezione w zakresie parametrów i stamtąd zostaje pobrana wartość. Problem pojawia się, gdy użyjemy tego samego identyfikatora co obliczany parametr – jak w przypadku z. Wtedy silnik zauważa, że z nie zostało jeszcze niezainicjalizowane wewnątrz zakresu parametrów. Sprzecznie z intuicją, zamiast szukać wartość w zakresie otaczającym, wyrzuca ReferenceError.

Podsumowanie

Wow. Wyszedłem z błędnego założenia, że to będzie najkrótszy z dotychczasowych wpisów. W końcu ile można ględzić o trzech stosunkowo prostych funkcjonalnościach. Nic bardziej mylnego. To, co proste i intuicyjne doczekuje się największej uwagi. Liczba proponowanych zastosowań przerosła moje najśmielsze oczekiwania. Coś czuję, że podobnie jak ja, będziecie wciskać spread i rest gdzie tylko się da. Wszelkie zażalenia, związane z uwagami seniorów podczas code review, możecie kierować na moją skrzynkę mailową ;).

Zachęcam do śledzenia bloga na facebooku. Wróciłem do codziennego korzystania z Twittera. Zapraszam do ćwierkania :).

W przyszłym tygodniu będzie o obietnicach. Na blogowe moralizowanie (jeszcze) brakuje mi czasu, więc skupimy się na programowaniu asynchronicznym.

Zadania praktyczne

Każdy z odnośników prowadzi do edytora online z treścią zadania i przygotowanym środowiskiem. Poćwicz, co to by zdobyta wiedza nie uleciała w eter.

Źródła

Zdjęcie tytułowe autorstwa:

Patrick Fore

Marcin Czarkowski

Cześć! Ja nazywam się Marcin Czarkowski, a to jest AlgoSmart - blog, na którym dzielę się wiedzą o ReactJS, JavaScript oraz CSS. Od niedawna tworzę materiały na YouTube, warto rzucić okiem :).