Moduł3 - Zajęcia 6 - Operacje spread/rest

Section1 Article1: Składnia spread i rest

Współczesny standard wprowadził nową składnię do pracy z iterowalnymi strukturami, takimi jak string, tablica czyli obiekt. Jego funkcjonalność i nazwa zależy od miejsca zastosowania.

Section1 Article2: spread: przekazywanie argumentów

Operacja ... (spread) umożliwia rozłożenie kolekcji elementów (tablicy, stringa lub obiektu) do miejsca, w którym oczekiwany jest zestaw różnych wartości. Istnieją pewne ograniczenia, na przykład nie można rozkładać tablicy do obiektu i odwrotnie.

Wytłumaczmy sobie to na analogii z pudełkiem jabłek. Stawiając pudełko na podłodze bez wyjmowania z niego jabłek, otrzymujemy odpowiednik tablicy wartości. Jeśli wysypać jabłka z pudełka na podłogę, następuje rozłożenie - kolekcja przetwarza się w zestaw oddzielnych wartości.

Istnieje pewna różnica - w JavaScript takie rozłożenie nie zmienia oryginalnej kolekcji, czyli tworzy kopię każdego elementu. Po rozłożeniu będziemy mieli zarówno pudełko pełne jabłek jak i kopię każdego z jabłek.

Dla przykładu, metoda Math.max(argumenty) wyszukuje i zwraca największy z argumentów (liczb), co oznacza, że nie oczekuje tablicy wartości, ale dowolnej ilości argumentów.

            const temps = [14, -4, 25, 8, 11];

            // Konsola wyświetli tablicę
            console.log(temps);
            // ❌ To nie zadziała, ponieważ przekazujemy całą tablicę
            console.log(Math.max(temps));// NaN

            // Konsola wyświetli zestaw oddzielnych liczb
            console.log(...temps);
            // ✅ Rozkładamy kolekcję elementów na oddzielne argumenty
            console.log(Math.max(...temps));// 25
          

Czyli zapis Math.max(...[14, -4, 25, 8, 11]), po interpretacji operatora ... zmieni się w Math.max(14, -4, 25, 8, 11) - składnia ... zwraca "rozpakowuje" tablicę, czyli rozkłada jej elementy na oddzielne argumenty.

Section1 Article3: spread: tworzenie nowej tablicy

Operacja ... (spread) pozwala również na utworzenie kopii tablicy lub "sklejenie" dowolnej liczby tablic w jedną nową. Wcześniej używaliśmy do tego metod slice() i concat(), ale operacja rozłożenia pozwala zrobić to samo w krótszej formie.

            const temps = [14, -4, 25, 8, 11];

            // To jest dokładna, niezależna kopia tablicy temps
            const copyOfTemps = [...temps];
            console.log(copyOfTemps);// [14, -4, 25, 8, 11]
          

W powyższym przykładzie mamy pudełko "jabłek" temps i chcemy stworzyć jego niezależną kopię. Bierzemy puste pudełko i wsypujemy do niego jabłka z oryginalnego pudełka temps - rozkładamy go do innej kolekcji. W tym przypadku pudełko temps nie zmieni się, nadal będzie zawierało jabłka, a nowe pudełko będzie zawierało ich dokładne kopie. W ten sposób unikamy problemów związanych z referencją.

W poniższym przykładzie wsypujemy jabłka z dwóch pudełek do nowego. Oryginalne pudełka (tablice) nie ulegną zmianie, ale nowe będą zawierały kopie wszystkich ich jabłek (elementów). Kolejność rozkładania jest ważna - wpływa na kolejność elementów w nowej tablicy.

            const lastWeekTemps = [14, 25, 11];
            const currentWeekTemps = [23, 17, 18];
            const allTemps = [...lastWeekTemps, ...currentWeekTemps];
            console.log(allTemps);// [14, 25, 11, 23, 17, 18]
          

Kolejność rozkładania ma znaczenie. Nazwy właściwości obiektów są niepowtarzalne, więc właściwości rozkładanego obiektu mogą nadpisać wartość istniejącej już właściwości, jeśli ich nazwy są takie same.

            const first = { propA: 5, propB: 10, propC: 50 };
            const second = { propC: 15, propD: 20 };

            const third = { ...first, ...second };
            console.log(third);// { propA: 5, propB: 10, propC: 15, propD: 20 }

            const fourth = { ...second, ...first };
            console.log(fourth);// { propA: 5, propB: 10, propC: 50, propD: 20 }
          

Gdyby jabłka w pudełku miały etykiety, to w jednym pudełku nie mogłyby znajdować się dwa jabłka z tymi samymi etykietami. Dlatego podczas wysypania drugiego pudełka wszystkie jabłka, których etykiety pokrywają się z tymi, które są już w nowym, zastąpią te, które znalazły się już w pudełku.

Podczas rozkładania możesz dodać właściwości w dowolne miejsca. Najważniejszą rzeczą do zapamiętania jest to, że nazwa właściwości jest unikalna i że jej wartość można nadpisać.

            const first = { propA: 5, propB: 10, propC: 50 };
            const second = { propC: 15 };

            const third = { propB: 20, ...first, ...second };
            console.log(third);// { propA: 5, propB: 10, propC: 15 }

            const fourth = { ...first, ...second, propB: 20 };
            console.log(fourth);// { propA: 5, propB: 20, propC: 15 }

            const fifth = { ...first, propB: 20, ...second };
            console.log(fifth);// { propA: 5, propB: 20, propC: 15 }
          

Section1 Article4: rest: zbiór wszystkich argumentów funkcji

Operacja ... (rest) umożliwia zebranie grupy niezależnych elementów w nową kolekcję. Syntaktycznie jest to bliźniak operacji rozłożenia, ale łatwo je rozróżnić — rozłożenie następuje, gdy ... znajduje się po prawej stronie operacji przypisania, a zbiór ma miejsce, gdy ... znajduje się po lewej stronie.

Wróćmy do analogii z jabłkami. Jeśli na podłodze są jabłka i mamy puste pudełko, to operacja rest pozwoli nam "zbierać" jabłka do pudełka. W takim przypadku oryginalne jabłka pozostaną na podłodze, a w pudełku będzie kopia każdego jabłka.

Jedno z zastosowań operacji rest znajdziemy podczas tworzenie funkcji, które mogą przyjmować dowolną liczbę argumentów.

            // Jak zadeklarować parametry funkcji tak,
            // aby można było przekazać dowolną liczbę argumentów?
            function multiply() {
                // ...
            }

            multiply(1, 2);
            multiply(1, 2, 3);
            multiply(1, 2, 3, 4);
          

Jeśli usuniemy cały "szum" z kodu i spojrzymy na argumenty i parametry funkcji, to argumenty znajdują się po prawej stronie operacji przypisania, a parametry po lewej, ponieważ wartości argumentów są przypisane do zadeklarowanych parametrów. Możesz więc "zebrać" wszystkie argumenty funkcji w jeden parametr za pomocą operacji rest.

            function multiply(...args) {
                console.log(args);// tablica wszystkich argumentów
            }

            multiply(1, 2);
            multiply(1, 2, 3);
            multiply(1, 2, 3, 4);
          

Nazwa parametru może być dowolna. Najczęściej nazywamy go args, restArgs albo otherArgs, czyli różnymi skrótami od arguments (będącego słowem kluczowym więc nie możemy go tutaj wykorzystać).

Section1 Article5: rest: zbiór części argumentów funkcji

Operacja ... (rest) umożliwia również zbiór do tablicy tylko części argumentów, poprzez zadeklarowanie zdefiniowanych parametrów przed "zbiorem".

            function multiply(firstNumber, secondNumber, ...otherArgs) {
                console.log(firstNumber);// Wartość pierwszego argumentu
                console.log(secondNumber);// Wartość drugiego argumentu
                console.log(otherArgs);// Tablica innych argumentów
            }

            multiply(1, 2);
            multiply(1, 2, 3);
            multiply(1, 2, 3, 4);
          

Wszystkie argumenty dla których zostaną zadeklarowane parametry przekażą im swoje wartości, reszta argumentów zostanie umieszczona w tablicy jeśli skorzystamy z tego zapisu. Operacja rest zbiera wszystkie pozostałe argumenty i dlatego musi znaleźć się na końcu w sygnaturze funkcji, w przeciwnym razie wystąpi błąd.

Section2 Article1: Destrukturyzacja obiektów

Podczas działania programów, dane przychodzą zazwyczaj w postaci tablic i obiektów, których wartości potrzebujemy zapisywać w zmiennych lokalnych. Aby to maksymalnie uprościć, współczesny standard udostępnia składnię przypisania destrukturyzującego.

Section2 Article2: Destrukturyzacja obiektów

Złożone dane są zawsze reprezentowane jako obiekt. Wielokrotne wywołania właściwości obiektu wizualnie zanieczyszczają kod.

            const book = {
                title: "The Last Kingdom",
                author: "Bernard Cornwell",
                genres: ["historical prose", "adventure"],
                isPublic: true,
                rating: 8.38,
            };

            const accessType = book.isPublic ? "publiczny" : "zamknięty";
            const message = `Książka ${book.title} autorstwa ${book.author} z oceną ${book.rating} ma dostęp ${accessType}.`
            // 'Książka The Last Kingdom autorstwa Bernard Cornwell z oceną 8.38 ma dostęp publiczny.'
          

Destrukturyzacja umożliwia "rozpakowanie" wartości właściwości obiektu do zmiennych lokalnych. Dzięki temu kod w miejscu ich użycia jest mniej "zaszumiony".

            const book = {
                title: "The Last Kingdom",
                author: "Bernard Cornwell",
                genres: ["historical prose", "adventure"],
                isPublic: true,
                rating: 8.38,
            };

            // Destrukturyzacja
            const { title, author, isPublic, rating, coverImage } = book;
            console.log(coverImage);// undefined

            const accessType = isPublic ? "publiczny" : "zamknięty";
            const message = `Książka ${title} autorstwa ${author} z oceną ${rating} ma dostęp w ${accessType}.`;
            // 'Książka The Last Kingdom autorstwa Bernard Cornwell z oceną 8.38 ma dostęp publiczny.'
          

Destrukturyzacja zawsze znajduje się po lewej stronie operacji przypisania. Zmiennym wewnątrz nawiasów klamrowych przypisywane są wartości właściwości obiektu. Jeśli nazwa zmiennej i nazwa właściwości są takie same, to przypisanie ma miejsce, w przeciwnym razie zostanie mu przypisane undefined. Kolejność, w jakiej zmienne są deklarowane w nawiasach klamrowych, nie jest ważna, ponieważ w obiekcie nazwy właściwości są unikalne.

Section2 Article3: Wartości domyślne

Aby uniknąć przypisania undefined podczas destrukturyzacji nieistniejących właściwości obiektu, możesz ustawić domyślne wartości zmiennych, które zostaną przypisane tylko wtedy, gdy obiekt nie ma właściwości o danej nazwie.

            const book = {
                title: "The Last Kingdom",
                author: "Bernard Cornwell",
            };

            // Dodajmy zdjęcie na okładkę, jeśli nie ma go w obiekcie książki
            const {
                title,
                coverImage = "https://via.placeholder.com/640/480",
                author,
            } = book;

            console.log(title);// The Last Kingdom
            console.log(author);// Bernard Cornwell
            console.log(coverImage);// https://via.placeholder.com/640/480
          

Section2 Article4: Zmiana nazwy zmiennej

Podczas destrukturyzacji możesz zmienić nazwę zmiennej, do której rozpakowana jest wartość właściwości. Aby to osiągnąć najpierw piszemy nazwę właściwości, z której chcemy uzyskać wartość, po czym wstawiamy dwukropek i wpisujemy nazwę zmiennej, w której ma być umieszczona wartość tej właściwości.

            const firstBook = {
                title: "The Last Kingdom",
                coverImage:
                "https://images-na.ssl-images-amazon.com/images/I/51b5YG6Y1rL.jpg",
            };

            const {
                title: firstTitle,
                coverImage: firstCoverImage = "https://via.placeholder.com/640/480",
            } = firstBook;

            console.log(firstTitle);// The Last Kingdom
            console.log(firstCoverImage);// https://images-na.ssl-images-amazon.com/images/I/51b5YG6Y1rL.jpg

            const secondBook = {
                title: "Sen śmiesznego człowieka",
            };

            const {
                title: secondTitle,
                coverImage: secondCoverImage = "https://via.placeholder.com/640/480",
            } = secondBook;

            console.log(secondTitle);// Sen śmiesznego człowieka
            console.log(secondCoverImage);// https://via.placeholder.com/640/480
          

Tego typu zapis możemy zrozumieć jako: "Utwórz zmienną firstTitle, w której należy umieścić wartość właściwości title z obiektu firstBook" itd.

Section2 Article5: Destrukturyzacja w pętlach

Podczas iteracji po tablicy obiektów z pętlą for...of często będzie wielokrotnie wywoływać obiekt.

            const books = [
            {
                title: "The Last Kingdom",
                author: "Bernard Cornwell",
                rating: 8.38,
            },

            {
                title: "Beside Still Waters",
                author: "Robert Sheckley",
                rating: 8.51,
            },
            ];

            for (const book of books) {
                console.log(book.title);
                console.log(book.author);
                console.log(book.rating);
            }
          

Aby zmniejszyć liczbę powtórzeń w kodzie, możesz zdestrukturyzować właściwości obiektu na zmienne lokalne w deklaracji pętli.

            const books = [
            {
                title: "The Last Kingdom",
                author: "Bernard Cornwell",
                rating: 8.38,
            },
            {
                title: "Beside Still Waters",
                author: "Robert Sheckley",
                rating: 8.51,
            },
            ];

            for (const book of books) {
                const { title, author, rating } = book;

                console.log(title);
                console.log(author);
                console.log(rating);
            }
          

Jeśli obiekt ma niewiele właściwości, destrukturyzację można przeprowadzić bezpośrednio w miejscu, w którym deklarowana jest zmienna book

            const books = [
            {
                title: "The Last Kingdom",
                author: "Bernard Cornwell",
                rating: 8.38,
            },
            {
                title: "Beside Still Waters",
                author: "Robert Sheckley",
                rating: 8.51,
            },
            ];

            for (const { title, author, rating } of books) {
                console.log(title);
                console.log(author);
                console.log(rating);
            }
          

Section2 Article6: Głęboka destrukturyzacja

Do destrukturyzacji właściwości obiektów zagnieżdżonych są używane te same zasady, jak w poprzednich trzech ćwiczeniach.

            const user = {
                name: "Jacques Gluke",
                tag: "jgluke",
                stats: {
                    followers: 5603,
                    views: 4827,
                    likes: 1308,
                },
            };

            const {
                name,
                tag,
                stats: { followers, views: userViews, likes: userLikes = 0 },
            } = user;

            console.log(name); // Jacques Gluke
            console.log(tag); // jgluke
            console.log(followers); // 5603
            console.log(userViews); // 4827
            console.log(userLikes); // 1308
          

Section3 Article1: Destrukturyzacja tablic

Przypisanie destrukturyzujące może być również użyte dla tablic, ale będzie się nieco różniło:

  • Użyj nawiasów kwadratowych [] zamiast nawiasów klamrowych {}
  • Zmiennym określonym w nawiasach kwadratowych [], będą po kolei przypisywane wartości elementów tablicy - kolejność zmiennych będzie więc miała znaczenie.

Dla przykładu weźmy tablicę kolorów, z której należy pobrać wartości każdego składnika koloru do osobnych zmiennych.

            const rgb = [200, 255, 100];
            const [red, green, blue] = rgb;

            console.log(`R:${red},G:${green},B:${blue}`);// "R:200,G:255,B:100"
          

Po słowie kluczowym const lub let umieszczamy otwierający i zamykający nawias kwadratowy, tak jak przy deklarowaniu tablicy. Wewnątrz nawiasów, oddzielone przecinkami, wskazujemy nazwy zmiennych, w których zostaną umieszczone wartości tablicy.

W wyniku zapisu powyżej zostaną utworzone 3 zmienne, a elementy zostaną w nich umieszczone w kolejności numeracji - od 0 do końca tablicy.

Podczas destrukturyzacji tablic wartość zmiennej można przypisać już po jej deklaracji. W praktyce jest to rzadko używane i zmniejsza czytelność.

            const rgb = [200, 255, 100];
            let red, green, blue;
            [red, green, blue] = rgb;
            console.log(`R:${red},G:${green},B:${blue}`);// "R:200,G:255,B:100"
          

Jeśli podamy więcej zmiennych niż elementów tablicy, zostaną im przypisane wartości undefined, możemy więc również skorzystać z zapisu wartości domyślnych.

            const rgb = [200, 100, 255];
            const [red, green, blue, alfa = 0.3] = rgb;
            console.log(`R:${red},G:${green},B:${blue},Alfa:${alfa}`);// "R:200,G:100,B:255,Alfa:0.3"
          

Czasami konieczna jest destrukturyzacja tylko pierwszych N elementów z tablicy i przechowywanie pozostałych w jednej zmiennej w postaci tablicy. Podczas destrukcji tablicy możesz rozpakować i przypisać pozostałe elementy tablicy do zmiennej za pomocą operacji ... (rest).

            const rgb = [200, 255, 100];
            const [red, ...colors] = rgb;
            console.log(red);// "200"
            console.log(colors);// [255, 100]
          

Elementy możemy też pominąć. Załóżmy, że musisz pobrać tylko ostatnią wartość z tablicy rgb. Taki zapis nie jest zbyt często używany, ale poznajmy go:

            const rgb = [200, 100, 255];
            const [, , blue] = rgb;
            console.log(`Blue: ${blue}`);// "Blue: 255"
          

Section4 Article1: Wzorzec "Obiekt parametru"

Jeśli funkcja przyjmuje więcej niż dwa lub trzy argumenty, bardzo łatwo jest pomylić się, w jakiej kolejności należy przekazać wartości. Rezultatem jest bardzo nieczytelny kod w miejscu jego wywołania i trudne do odnalezienia na pierwszy rzut oka błędy.

            function doStuffWithBook(title, numberOfPages, downloads, rating, public) {
                // wykorzystanie parametrów
                console.log(title);
                console.log(numberOfPages);
                // dalsza część kodu
            }

            // ❌ Co to jest 736? Co to jest 10283? Co to jest true?
            doStuffWithBook("The Last Kingdom", 736, 10283, 8.38, true);
          

Wzorzec "Obiekt parametru" pomaga rozwiązać ten problem, zastępując zestaw parametrów tylko jednym — obiektem o nazwanych właściwościach

            function doStuffWithBook(book) {
                // wykorzystanie parametru (obiektu)
                console.log(book.title);
                console.log(book.numberOfPages);
                // dalsza część kodu
            }
          

Następnie podczas wywołania przekazujemy jeden obiekt z niezbędnymi właściwościami.

            // ✅ wykorzystanie wszystkich zmiennych jest jasne
            doStuffWithBook({
                title: "The Last Kingdom",
                numberOfPages: 736,
                downloads: 10283,
                rating: 8.38,
                isPublic: true,
            });
          

Kolejnym plusem jest to, że możesz również zdestrukturyzować obiekt w parametrze book. Można to zrobić na dwa sposoby, albo w ciele funkcji.

            function doStuffWithBook(book) {
                const { title, numberOfPages, downloads, rating, isPublic } = book;
                console.log(title);
                console.log(numberOfPages);
                // dalsza część kodu
            }
          

Lub od razu w sygnaturze funkcji, oba zapisy są poprawne

            function doStuffWithBook({
                title,
                numberOfPages,
                downloads,
                rating,
                isPublic,
            }) {
                console.log(title);
                console.log(numberOfPages);
                // dalsza część kodu
            }