Moduł1 - Zajęcia2 - Rozgałęzienia i cykle

Section1: Instrukcje warunkowe

Instrukcje warunkowe (rozgałęzienia kodu) są wykorzystywane do wykonywania innego kodu w zależności od spełnionych warunków. Zasada działania jest prosta - wynik warunku sprowadza się do wartości boolean true lub false, po czym wykonywany kod programu jest kierowany w zależności od tego czy warunek jest spełniony czy nie (true lub false)

Instrukcja if

            if (warunek) {
            // ciało if
            }
          

Warunkiem nazywamy wyrażenie które sprowadzi się do wartości truthy lub falsy. Warunki są umieszczane po instrukcji if w nawiasach. Jeśli warunek sprowadzi się do true, wtedy wykonywany jest kod w nawiasach klamrowych ciała if.

            let cost = 0;
            const subscription = "pro";
            if (subscription === "pro") {
                cost = 100;
            }
            console.log(cost);// 100
          

Jeśli warunek nie jest spełniony i zwraca false, kod w nawiasach klamrowych nie zostanie wykonany.

            let cost = 0;
            const subscription = "free";
            if (subscription === "pro") {
                cost = 100;
            }
            console.log(cost);// 0
          
            if (warunek) {
                // ciało if
            } else {
                // ciało else
            }
          

To dodatkowy element składni if. Jeśli warunek nie jest spełniony czyli sprowadza się do false, zostanie wykonany kod, który znajduje się w nawiasach klamrowych po operatorze else.

            let cost;
            const subscription = "free";
            if (subscription === "pro") {
                cost = 100;
            } else {
                cost = 0;
            }
            console.log(cost);// 0
          

Jeśli warunek zostanie spełniony i zwróci true, ciało bloku else zostanie zignorowane.

            let cost;
            const subscription = "pro";
            if (subscription === "pro") {
                cost = 100;
            } else {
                cost = 0;
            }
            console.log(cost);// 100
          

Instrukcja else nie może występować samodzielnie, zawsze następuje po instrukcji if

Instrukcja else...if

Konstrukcja if...else może sprawdzić i zareagować na wykonanie lub niewykonanie wyłącznie jednego warunku z kilku możliwości.

Blok else...if pozwala po else dodać jeszcze jeden operator if z warunkiem. Na końcu łańcucha możemy napisać klasyczny blok else, który zostanie wykonany tylko wtedy, gdy żaden z wcześniejszych warunków nie będzie sprowadzał się do true.

            let cost;
            const subscription = "premium";
            if (subscription === "free") {
                cost = 0;
            } else if (subscription === "pro") {
                cost = 100;
            } else if (subscription === "premium") {
                cost = 500;
            } else {
                console.log("Invalid subscription type");
            }
            console.log(cost);// 500
          

Przy pierwszym spełnieniu się warunku w if kod przestanie sprawdzać kolejne warunki i zostanie wykonany tylko jeden blok kodu odpowiadający danemu warunkowi. Taki zapis możemy rozumieć jako: szukam pierwszego spełnionego warunku, reszta instrukcji zostaje zignorowana, jeśli żaden warunek nie jest spełniony wykonam kod w ostatnim bloku czyli else

Ternary operator

Operator trójargumentowy (czasem nazywamy warunkowym) jest używany jako bardziej bardziej zwięzły zapis instrukcji if ... else, w sytuacjach, gdy trzeba przypisać tej samej zmiennej różne wartości w zależności tego czy warunek został spełniony.

            "warunek" ? "wyrażenie_jeśli_warunek_spełniony" : "wyrażenie_jeśli_warunek_nieprawdziwy"

Działa to według następującego schematu:

  • warunek zostaje obliczony
  • Jeśli warunek jest prawdziwy - true, wykonuje się wyrażenie po ? i jego wartość jest zwracana
  • Jeśli warunek nie jest prawdziwy - false, wykonuje się wyrażenie po : i jego wartość jest zwracana
  • Zwróconą wartość możemy następnie przypisać do zmiennej lub wykorzystać w inny sposób.
            let type;
            const age = 20;
            if (age >= 18) {
                type = "adult";
            } else {
                type = "child";
            }
            console.log(type);// "adult"
          

Zróbmy refaktoryzacje zastępując if...else operatorem ternary.

            const age = 20;
            const type = age >= 18 ? "adult" : "child";
            console.log(type);// "adult"
          

Napiszmy algorytm odnajdujący która liczba (z podanych dwóch) jest większa

            const num1 = 5;
            const num2 = 10;
            let biggerNumber;
            if (num1 > num2) {
                biggerNumber = num1;
            } else {
                biggerNumber = num2;
            }
            console.log(biggerNumber);// 10
          

Kod działa poprawnie, otrzymujemy największą liczbę, ale biorąc pod uwagę prostotę operacji wykonywanych w blokach if...else warto wykorzystać operator ternary co znacznie skróci kod.

            const num1 = 5;
            const num2 = 10;
            const biggerNumber = num1 > num2 ? num1 : num2;
            console.log(biggerNumber);// 10
          

Operator trójargumentowy powinien być używany tylko w prostych operacjach przypisania lub otrzymywania danych. Złą praktyką (antywzorem) jest używanie go do bardziej złożonych operacji.

Section3: Instrukcja switch

W niektórych przypadkach możemy uniknąć trudności związanych z odczytywaniem złożonych bloków if ... else. Czasem można użyć bardziej «płaskiej» składni - instrukcji switch.

Przydatność tej konkretnej instrukcji jest ograniczona do zadań w których mamy wiele możliwych scenariuszy ale jedyne warunki logiczne które nas interesują to porównania zmiennej z konkretnymi wartościami (znanymi przez nas podczas pisania kodu).

Składnia tego bloku składa się z

  • switch (value) - gdzie value to zmienna którą porównujemy
  • oraz przypadki case value - gdzie podajemy możliwe wartości dla zmiennej.

Do porównania użyty zostanie operator ścisłej równości ===. Oznacza to, że nie możemy skorzystać z takich operatorów jak < czy !==

            switch (value) {
                case value:
                instrukcja; 
                break;

                case value:
                instrukcja; 
                break;

                default:
                instrukcje;
            }
          

Value w bloku switch (value) jest zmienną zawierającą string lub liczbę, która jest porównywana według ścisłej równości ze wszystkimi wartościami podanymi w blokach case value . Kolejność bloków ma znaczenie i sprawdzenia odbywają się z góry na dół.

Instrukcja break na końcu każdego bloku case jest potrzebna do przerwania dalszego wykonywania bloku jeżeli dany case został spełniony. Wtedy JavaScript przejdzie do dalszych instrukcji pod blokiem switch

Jeśli nie doszło do dopasowania wartości w żadnym case, powinniśmy zdefiniować kod który należy wykonać domyślnie, podobnie jak w bloku else dla instrukcji if ... else. W tym celu po wszystkich blokach case, dodawany jest blok default. Operator break po bloku default nie jest wymagany, ponieważ jeśli ten blok się wykona, to będzie to ostatnie co zrobi dany switch

            let cost;
            const subscription = "premium";

            switch (subscription) {
                case "free":
                cost = 0;
                break;

                case "pro":
                cost = 100;
                break;

                case "premium":
                cost = 500;
                break;

                default:
                console.log("Invalid subscription type");
            }
            console.log(cost);// 500
          

Jeśli nie ma operatora break to gdy zostanie spełniony jakiś warunek case, wszystkie kolejne bloki kodu będą wykonywane po kolei do momentu natrafienia na operator break lub koniec bloku switch, co może doprowadzić do niepożądanych konsekwencji.

Section4: Variable scope

Zakres widoczności zmiennych (variable scope) - tak określamy dostępność zmiennych w określonym miejscu kodu.

Globalny variable scope jest używany domyślnie. Wszędzie mamy dostęp do zadeklarowanej w nim zmiennej. Na przykład zmienna value, zadeklarowana w zasięgu globalnym, czyli poza jakimkolwiek blokiem, będzie dostępna w dowolnym miejscu w kodzie.

            const value = 5;
            if (true) {
                console.log("Block scope: ", value);// 5
            }
            console.log("Global scope: ", value);// 5
          

Wszystkie konstrukcje używające nawiasów klamrowych {} (warunki, pętle, funkcje itp.) tworzą nowy zakres lokalny, a zmienne zadeklarowane w tym zakresie przy użyciu let lub const nie są dostępne poza tym blokiem.

            if (true) {
                const value = 5;
                console.log("Block scope: ", value);// 5
            }
            console.log("Global scope: ", value);// ReferenceError: value is not defined
          

Głębokość zagnieżdżania zakresów nie jest ograniczona i niezależnie o niej, zakresy będą działać zgodnie z jedną zasadą - scope ma dostęp do wszystkich zadeklarowanych zmiennych, które znajdują się wyżej w hierarchii zagnieżdżania, ale nie ma dostępu do zmiennych zadeklarowanych w zagnieżdżonych w nim zakresach.

Stwórzmy kilka zakresów i nadajmy im nazwy dla jasności.

  • Globalny zakres jest ustawiony domyślnie, stwórzmy w nim zmienną global
  • Wykorzystajmy operator if dla utworzenia zakresu bloku block A
  • Wewnątrz zakresu block A stwórzmy jeszcze jeden operator if, który utworzy zakres bloku block B
  • Na tym samym poziome z block A, stwórzmy zakres bloku block C wykorzystując ten sam operator co wcześniej czyli if
            const global = "global";

            if (true) {
                const blockA = "block A";

                // Możemy użyć globalnego zakresu i lokalnego zakresu A
                console.log(global);// 'global'
                console.log(blockA);// block A

                // Zmienne blockB i blockC nie są odznalezione w dostępnych zakresach.
                // Pojawi się błąd wykorzystania zmiennej.
                console.log(blockB);// ReferenceError: blockB is not defined
                console.log(blockC);// ReferenceError: blockC is not defined

                if (true) {
                    const blockB = "block B";

                    // Możemy użyć globalnego zakresu + zewnętrznego A + lokalnego B
                    console.log(global);// global
                    console.log(blockA);// block A
                    console.log(blockB);// block B

                    // Zmienna blockC nie jest odnaleziona w dostępnych zakresach.
                    // Pojawi się błąd wykorzystania zmiennej.
                    console.log(blockC);// ReferenceError: blockC is not defined
                }
            }

            if (true) {
                const blockC = "block C";

                // Możemy użyć globalnego zakresu + lokalnego C
                console.log(global);// global
                console.log(blockC);// block C

                // Zmienne blockA i blockB nie są odnalezione w dostępnych zakresach.
                // Pojawi się błąd wykorzystania zmiennej.
                console.log(blockA);// ReferenceError: blockA is not defined
                console.log(blockB);// ReferenceError: blockB is not defined
            }

            // Użyjmy tylko globalnego zakresu
            console.log(global);// global

            // Zmienne blockA, blockB i blockC 
            // nie są odznalezione w dostępnych zakresach.
            // Pojawi się błąd wykorzystania zmiennej.
            console.log(blockA);// ReferenceError: blockA is not defined
            console.log(blockB);// ReferenceError: blockB is not defined
            console.log(blockC);// ReferenceError: blockC is not defined
          

Bądź czujny wykorzystując zakresy bloków i zmiennych utworzonych wewnątrz tych zakresów. Często właśnie dostępność i zakresy stanowią duży problem dla osób początkujących

Section5: Pętle

Typowym problemem dla programisty jest wykonanie tego samego kodu zdefiniowaną ilość razy. Przykładem będzie usuwanie klientów z listy jeden po drugim lub sortowanie kwot wynagrodzeń dla pracowników. Dla każdej z tych czynności trzeba wykonać ten sam kod więcej niż raz. Możemy do takich zadań wykorzystać pętle, które ułatwiają pracę z powtarzającym się kodem.

  • Pętla - instrukcja w językach programowania, która pozwala wielokrotne wykonać ten sam kod.
  • Ciało pętli - kod który zostanie wykonany dla każdego przejścia/obrotu pętli
  • Iteracja - jednorazowe wykonanie ciała pętli, nazywane też obrotem lub przejściem pętli.
  • Warunek - wyrażenie, które określa, czy po danej iteracji ma nastąpić kolejna, czy pętla zakończyła swoje działanie, sprawdzany po każdej iteracji.
  • Licznik / Iterator Variable - zmienna liczbowa, która przechowuje w której iteracji jesteśmy. Licznik jest opcjonalny i niekoniecznie musi być tylko jeden, warunek zatrzymania pętli może zależeć od wielu zmiennych.

Pętla while

Pętla z warunkiem wstępnym — pętla, która działa tak długo, aż nie zostanie spełniony określony warunek. Ten warunek jest sprawdzamy przed każdą iteracją, przez co pętla może nie wykonać się ani razu. Stanie się to w przypadku, gdy warunek na samym początku jest nieprawdziwy.

            while (condition) {
                // Kod, ciało pętli (body)
            }
          

Konstrukcja while tworzy pętle, który wykonuje blok kodu, jednocześnie sprawdzając czy warunek jest prawdziwy - true.

  • condition, warunek, jest sprawdzany przed każdą iteracją.
  • Jeśli condition jest prawdziwy true, operator while wykonuje kod w body pętli.
  • Jeśli condition jest nieprawdziwy false, pętla jest zatrzymana i wykonane zostaną dalsze instrukcje w naszym kodzie

Stwórzmy licznik.

            let counter = 0;
            while (counter < 10) {
                console.log("counter: ", counter);
                counter += 1;
            }
          

Zapełniajmy miejsca w hotelu do tego momentu, aż liczba klientów stanie się równa dozwolonemu maksimum.

            let clientCounter = 18;
            const maxClients = 25;

            while (clientCounter < maxClients) {
                console.log(clientCounter);
                clientCounter += 1;
            }
          

Pętla do...while

Pętla z warunkiem końcowym - pętla, w której warunek jest sprawdzany po wykonaniu ciała pętli. Z tej z pozoru drobnej różnicy wynika, że ciało jest zawsze wykonywane przynajmniej raz niezależnie od warunku.

            do {
                // body
            } while (condition);
          

Konstrukcja do...while tworzy pętlę, który wykonuje kod do tego momentu aż condition nie zwróci wartość false.

W przeciwieństwie do pętli while, pętla do...while zawsze wykonuje swoje body przynajmniej jeden raz, zanim będzie ewaluować condition.

Wewnątrz pętli należy wprowadzić zmiany zmiennej decydującej o wartości wyrażenie, aby upewnić się, że po iteracjach wyrażenie ma wartość false. W przeciwnym razie pętla będzie nieskończona.

Schemat bloku pętli do-while

            let password = "";

            do {
                password = prompt("Wprowadź hasło dłuższe niż 4 znaki", "");
            } while (password.length < 5);

            console.log("Hasło zostało wprowadzone: ", password);
          

Pętla for

Pętla z licznikiem - pętla, w którym po każdej iteracji licznik zostaje zmieniony wedle naszych instrukcji, w momencie gdy licznik przestaje spełniać warunek pętla zatrzyma się.

W większości języków programowania istnieje instrukcja for, w której określamy licznik, wymaganą liczbę iteracji i wyrażenie na podstawie którego licznik zmienia się po każdej iteracji (czasem nazywamy to krokiem pętli).

            for (initialization; condition; post-expression) {
                // statements
            }
          

Algorytm wykonania pętli for:

  • Inicjalizacja (initialization) - wykonywana jeden raz przed rozpoczęciem pętli. Służy do tworzenia zmiennej-licznika i określenia jej początkowej wartości.
  • Warunek (condition) - wyrażenie wykonujące ocenę przed każdą iteracją (powtórzeniem) pętli Ciało pętli jest wykonywane tylko wtedy, gdy wyrażenie jest prawdziwe true. Pętla kończy się jeśli warunek zostanie ewaluowany na wartość nieprawdziwą false.
  • Post-expression - wykonywane na koniec każdej iteracji przed sprawdzeniem warunków. Używane jest do aktualizacji zmiennej-licznika.
  • Ciało (statements) - zestaw instrukcji do wykonania przy każdym powtórzeniu. Wykonywane, jeśli wynikiem wyrażenia warunku jest true.

Zmienne-liczniki zwykle nazywamy literami i, j lub k.

            for (let i = 0; i <= 20; i += 5) {
                console.log(i);
            }
          

Możemy na przykład zadeklarować zmienną-licznik i, inicjalizujemy ją z wartością 0 i pętla ma być wykonywana tak długo, aż i <= 10, czyli warunek sprowadza się do true. Po każdej iteracji licznik jest zwiększany o 5.

W ciele pętli policzmy sumę wartości które przyjmie licznik podczas trwania działania pętli.

            const target = 10;
            let sum = 0;

            for (let i = 0; i <= target; i += 5) {
                sum += i;
            }

            console.log(sum); // 15
          

Przypomnijmy sobie o operacji a % b i pokażmy resztę po dzieleniu wykorzystując pętlę.

            const max = 4;

            for (let i = 0; i < max; i += 1) {
                console.log(`${max} % ${i} = `, max % i);
            }

            // '4 % 0 = ' NaN
            // '4 % 1 = ' 0
            // '4 % 2 = ' 0
            // '4 % 3 = ' 1
          

Operator break

Możemy w dowolnym momencie przerwać wykonanie pętli. Aby to zrobić wykorzystujemy operator break, który zakończy wykonywanie pętli i sprawia, że interpreter przejdzie do kodu znajdującego się pod naszą pętlą.

W poniższej pętli sprawdzamy czy wartość licznika wynosi 3. Jak tylko zostanie spełniony taki warunek if, pętla przestanie wykonywać się (zostanie przerwana).

            for (let i = 0; i <= 5; i += 1) {
                console.log(i);
                if (i === 3) {
                    console.log("osiągneliśmy 3, przerywamy wykonywanie pętli");
                    break;
                }
            }
            console.log("wiadomość po pętli");
          

Operator continue

Nie przerywa naszej pętli całkiem, jedynie aktualną iterację. continue używamy, gdy wiemy, że w bieżącej iteracji pętli nie musimy wykonywać dalszej części kodu więc oszczędzamy zasoby i przechodzimy do kolejnego obrotu.

Wykorzystajmy pętlę do wyświetlenia tylko liczb nieparzystych. Dla parzystych i używamy operatora continue, wykonywanie obrotu zostaje zatrzymane i przechodzimy do następnej iteracji.

            const number = 10;

            for (let i = 0; i < number; i += 1) {
                if (i % 2 === 0) {
                    continue;
                }
                console.log("Nieparzyste i: ", i); // 1, 3, 5, 7, 9
            }