Pętle Zaawansowane

BREAK i CONTINUE

Używanie instrukcji break i continue w pętlach JavaScriptu pozwala na precyzyjną kontrolę nad ich wykonaniem, umożliwiając modyfikację standardowego przepływu sterowania.Oto główne powody i zastosowania:1. Kiedy używać break?Instrukcja break służy do natychmiastowego zakończenia (przerwania) bieżącej pętli, niezależnie od tego, czy warunek jej kontynuacji został spełniony.

  • Optymalizacja wydajności: Jeśli szukasz określonego elementu w dużej tablicy i go znajdziesz, nie ma sensu kontynuować iteracji przez resztę danych. Użycie break przerywa pętlę, oszczędzając czas procesora.
  • Obsługa błędów lub warunków specjalnych: Możesz przerwać pętlę, gdy wystąpi błąd lub gdy dalsze przetwarzanie danych jest bezcelowe.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];

for (let i = 0; i < numbers.length; i++) {
    if (numbers[i] === 5) {
        console.log("Znaleziono 5! Przerywam pętlę.");
        break; // Pętla kończy działanie w tym momencie
    }
    console.log(numbers[i]);
}
// Wynik:
// 1
// 2
// 3
// 4
// Znaleziono 5! Przerywam pętlę.

2. Kiedy używać continue?Instrukcja continue służy do pominięcia bieżącej iteracji pętli i natychmiastowego przejścia do następnej iteracji (sprawdzenia warunku kontynuacji i wykonania kolejnego kroku). Pętla nie jest przerywana całkowicie, a jedynie dany cykl zostaje pominięty.

  • Filtrowanie danych w locie: Pozwala na ignorowanie określonych wartości lub warunków bez przerywania przetwarzania pozostałych elementów w pętli.
  • Uproszczenie logiki zagnieżdżonej: Zamiast używać skomplikowanych instrukcji if...else lub głęboko zagnieżdżonych bloków kodu dla logiki pomijania, continue pozwala utrzymać kod bardziej płaskim i czytelnym.
const numbers = [1, 2, 3, 4, 5, 6];

for (let i = 0; i < numbers.length; i++) {
    if (numbers[i] % 2 === 0) {
        console.log(`Pomięto liczbę parzystą: ${numbers[i]}`);
        continue; // Pomija resztę kodu w tej iteracji i przechodzi do następnej
    }
    console.log(`Przetworzono liczbę nieparzystą: ${numbers[i]}`);
}
// Wynik:
// Przetworzono liczbę nieparzystą: 1
// Pomięto liczbę parzystą: 2
// Przetworzono liczbę nieparzystą: 3
// Pomięto liczbę parzystą: 4
// Przetworzono liczbę nieparzystą: 5
// Pomięto liczbę parzystą: 6

Podsumowując, break i continue są narzędziami, które dają programiście większą kontrolę nad przepływem pętli, co prowadzi do bardziej efektywnego, zoptymalizowanego i czytelnego kodu, pozwalając na eleganckie radzenie sobie z konkretnymi scenariuszami bez potrzeby stosowania dodatkowych zmiennych flagowych czy skomplikowanej logiki warunkowej.

Złożoność czasowa: Wpływa na złożoność, ponieważ może zredukować liczbę iteracji, które faktycznie zostaną wykonane. W skrajnym przypadku, gdy pętla zawiera break, złożoność może przejść z O(n) do O(1), jeśli element zostanie znaleziony w pierwszej iteracji. 


PĘTLE FOR...IN I FOR...OF

Pętle for...in i for...of w JavaScript służą do iteracji (przechodzenia) po różnych typach danych, ale różnią się fundamentalnie tym, po czym iterują

  • Pętla for...in iteruje po nazwach właściwości (kluczach) obiektu.
  • Pętla for...of iteruje bezpośrednio po wartościach obiektów iterowalnych, takich jak tablice (ang. arrays), ciągi znaków (ang. strings), Mapy i Set-y. 

Pętla for...in
Pętla for...in przeznaczona jest przede wszystkim do pracy z obiektami. Iteruje ona po wszystkich wyliczalnych właściwościach obiektu, w tym także tych dziedziczonych z prototypu (choć zazwyczaj zaleca się używanie hasOwnProperty() w celu uniknięcia dziedziczonych właściwości). Składnia

for (let key in object) {
  // wykonaj kod dla każdej właściwości (klucza)
}

Przykład użycia z obiektem

const user = {
  name: 'Jan',
  age: 30,
  city: 'Warszawa'
};

for (let key in user) {
  console.log(key + ": " + user[key]);
  // Output:
  // name: Jan
  // age: 30
  // city: Warszawa
}

Ważna uwaga dotycząca tablic
Chociaż technicznie można używać for...in do iteracji po tablicach (kluczami będą indeksy), nie jest to zalecane. Pętla zwraca indeksy jako ciągi znaków (ang. strings), a nie liczby, a kolejność iteracji nie zawsze jest gwarantowana.

Pętla for...of
Pętla for...of została wprowadzona w standardzie ES6 i służy do iteracji po wartościach kolekcji, które są iterowalne. Obejmuje to tablice, ciągi znaków, Mapy, Set-y, a także inne obiekty, które zdefiniowały swój iterator.

Składnia

for (let value of iterableObject) {
  // wykonaj kod dla każdej wartości
}

Przykład użycia z tablicą

const colors = ['czerwony', 'zielony', 'niebieski'];

for (let color of colors) {
  console.log(color);
  // Output:
  // czerwony
  // zielony
  // niebieski
}

Przykład użycia z ciągiem znaków

const greeting = "Hello";

for (let char of greeting) {
  console.log(char);
  // Output:
  // H
  // e
  // l
  // l
  // o
}

Ważna uwaga dotycząca obiektów
Obiekty (te "zwykłe", a nie np. Mapy) domyślnie nie są iterowalne, więc nie można użyć for...of bezpośrednio na nich. Aby iterować po obiekcie za pomocą for...of, należy użyć wbudowanych metod, które zwracają iterowalne tablice: 

  • Object.keys(obj) – zwraca tablicę kluczy.
  • Object.values(obj) – zwraca tablicę wartości.
  • Object.entries(obj) – zwraca tablicę par [klucz, wartość]

Podsumowanie różnic i zastosowań

Cecha for...infor...of
Iteruje poNazwach właściwości (kluczach)Wartościach elementów
Główne zastosowanieObiektyTablice, ciągi znaków, Mapy, Set-y
Typ zwracany (dla tablic)Indeksy jako stringWartości jako ich typ (number, string itp.)
Kolejność iteracjiNie jest gwarantowana (dla obiektów)Gwarantowana (zgodna z kolejnością elementów)

Używaj for...in, gdy potrzebujesz dostępu do nazw właściwości obiektu, a for...of, gdy chcesz przetwarzać wartości z kolekcji iterowalnej


TECHNIKI OPTYMALIZACJI
Optymalizacja pętli JavaScript polega na minimalizacji powtarzalnych obliczeń i kosztownych operacji wewnątrz pętli w celu poprawy wydajności.

1. Cachowanie (buforowanie) długości:
Powtarzające się odwoływanie się do właściwości length tablicy w warunku pętli zmusza silnik JavaScript do ponownego jej obliczania w każdej iteracji. Zbuforowanie długości przed pętlą eliminuje ten narzut.
Jak używać:javascript

// Zły przykład (mniej wydajny)
for (let i = 0; i < array.length; i++) {
  // operacje
}

// Dobry przykład (zoptymalizowany)
const length = array.length; // Buforowanie długości
for (let i = 0; i < length; i++) {
  // operacje
}

Można również zbuforować długość w samej deklaracji pętli:

for (let i = 0, len = array.length; i < len; i++) {
  // operacje
}


2. Minimalizowanie dostępu do DOM:
Dostęp do Document Object Model (DOM) i manipulowanie nim jest jedną z najbardziej kosztownych operacji w przeglądarce. Aktualizowanie DOM w każdej iteracji pętli powoduje wielokrotne przerysowywanie (reflow/repaint) strony, co spowalnia aplikację. Jak używać:

  • Zbiorcze aktualizacje DOM: Zamiast aktualizować element za każdym razem, gromadź zmiany w pamięci (np. używając Document Fragment lub budując ciąg znaków HTML), a następnie zaktualizuj DOM tylko raz, poza pętlą.
// Zły przykład
for (let i = 0; i < items.length; i++) {
  document.getElementById('container').innerHTML += `<div>${items[i]}</div>`;
}

// Dobry przykład (zoptymalizowany)
const container = document.getElementById('container');
let html = '';
for (let i = 0; i < items.length; i++) {
  html += `<div>${items[i]}</div>`;
}
container.innerHTML = html; // Jednorazowa aktualizacja DOM

  1. Odwrotna iteracja
    Odwrócenie kierunku pętli, tj. iterowanie od końca do początku, może zapewnić niewielką poprawę wydajności, ponieważ warunek i-- jest często prostszy do przetworzenia niż i < length
    Jak używać:
for (let i = array.length; i--;) {
  // operacje na array[i]
}

4. Użycie operatorów pre-inkrementacji (++i)
W tradycyjnych pętlach for, użycie pre-inkrementacji (++i) jest marginalnie szybsze niż post-inkrementacji (i++), ponieważ ta pierwsza nie tworzy tymczasowej kopii wartości przed jej inkrementacją. Różnica jest zazwyczaj pomijalna w nowoczesnym kodzie, ale jest to uznana technika mikrooptymalizacji. 

Jak używać

for (let i = 0; i < length; ++i) { // Użycie ++i
  // operacje

5. Wybieranie odpowiedniego typu pętli
W zależności od scenariusza, różne typy pętli mogą być bardziej wydajne. 

  • for loop jest często najszybszy w przypadku operacji krytycznych dla wydajności na dużych tablicach, ponieważ oferuje pełną kontrolę nad iteracją.
  • for...of jest czystszy, bardziej czytelny i zalecany, gdy nie potrzebujesz dostępu do indeksu, ale może być nieco wolniejszy niż tradycyjny for.
  • map()filter()reduce() i inne funkcje wyższego rzędu są świetne do deklaratywnych transformacji i często optymalizowane wewnętrznie, co prowadzi do czystszego kodu. Należy ich używać, gdy czytelność i ekspresywność kodu są priorytetem, a wydajność nie jest absolutnie krytyczna. 


6. Użycie map zamiast zagnieżdżonych pętli:
Używaj metody map zamiast zagnieżdżonych pętli, aby transformować elementy tablicy w bardziej czytelny i funkcyjny sposób, zamiast modyfikować tablicę w miejscu. map tworzy nową tablicę, co jest zgodne z zasadami programowania funkcyjnego i unika efektów ubocznych. Jest to przydatne do operacji takich jak podwajanie wartości, formatowanie danych czy ekstrakcja poszczególnych właściwości obiektów. Dlaczego używać map zamiast zagnieżdżonych pętli

  • Czytelność i zwięzłość: Kod z map jest często krótszy i łatwiejszy do zrozumienia, ponieważ deklaratywnie opisuje, co ma się stać z każdym elementem.
  • Niezmienność (Immutability): Metoda map tworzy nową tablicę i pozostawia oryginalną bez zmian. Jest to zgodne z zasadami programowania funkcyjnego i zapobiega nieoczekiwanym zmianom danych w innych częściach kodu.
  • Unikanie efektów ubocznych: Zagnieżdżone pętle (np. forEach lub tradycyjne for) często służą do modyfikowania istniejących tablic lub zmiennych, co może prowadzić do złożoności i błędów. map dedykowany jest do tworzenia nowych danych.

Jak używać mapmap() jest metodą tablicową, która przyjmuje funkcję zwrotną i stosuje ją do każdego elementu oryginalnej tablicy, zwracając nowy element do nowej tablicy. 

Przykład: Ekstrakcja danych z tablicy obiektów.javascript

const uzytkownicy = [
  { imie: "Anna", wiek: 30 },
  { imie: "Piotr", wiek: 25 }
];
const imiona = uzytkownicy.map(uzytkownik => uzytkownik.imie);
console.log(imiona); // Wynik: ["Anna", "Piotr"]

Przykład: Zamiana tablicy liczb na tablicę ich kwadratów.javascript

const liczby = [1, 2, 3];
const kwadraty = liczby.map(liczba => liczba * liczba);
console.log(kwadraty); // Wynik: [1, 4, 9]

Kiedy zagnieżdżone pętle są nadal potrzebne

  • Gdy potrzebujesz wykonać złożoną logikę w obrębie pętli: Jeśli wewnątrz pętli musisz wykonać operacje, które wpływają na wiele elementów lub zewnętrzne zmienne, zagnieżdżona pętla może być bardziej odpowiednia.
  • Gdy nie tworzysz nowej tablicy: Jeśli celem jest wykonanie akcji, a nie stworzenie nowej tablicy (np. proste logowanie każdego elementu), forEach może być prostszym rozwiązaniem.
  • Do iterowania po nie-tablicach: Metody iteracyjne takie jak map działają na tablicach. Do iterowania po innych kolekcjach, np. obiektach, nadal potrzebne są tradycyjne pętle lub metody specyficzne dla danego typu danych.

Najważniejszą zasadą optymalizacji jest unikanie przedwczesnej optymalizacji. Skup się najpierw na napisaniu czystego, zrozumiałego i działającego kodu. Jeśli napotkasz rzeczywiste problemy z wydajnością (co możesz sprawdzić za pomocą narzędzi takich jak console.time()), wówczas zastosuj powyższe techniki w konkretnych, wąskich gardłach aplikacji. 


Czym jest iterator?
W JavaScript, iterator to obiekt, który definiuje sekwencję i pozwala na jej iterowanie, czyli sekwencyjne odwoływanie się do kolejnych elementów. Jest to obiekt implementujący protokół iteratora, który posiada metodę next(). Ta metoda zwraca obiekt z dwiema właściwościami: value (wartość bieżącego elementu) i done (wartość logiczna wskazująca, czy sekwencja się zakończyła). 

  • Dostęp do elementów: Iterator pozwala na przechodzenie przez elementy kolekcji (np. tablicy czy obiektu) jeden po drugim, w określonej kolejności.
  • Zapamiętywanie pozycji: Każde wywołanie metody next() przechodzi do następnego elementu, a iterator zapamiętuje swoją aktualną pozycję w sekwencji.
  • Protokół iteratora: Obiekt-iterator musi mieć metodę next().
  • Wynik next(): Metoda next() zwraca obiekt { value: current_element, done: boolean }.
    • value: bieżący element sekwencji.
    • donetrue, gdy iterator zakończył już wszystkie elementy, false w przeciwnym wypadku.
  • Uniwersalność: Iteratory są przydatne, gdy chcemy przechodzić przez różne struktury danych lub gdy typy tych struktur nie są z góry znane, jak opisano w artykule na Refactoring.Guru. 

Jak sprawdzić czy dany obiekt jest "ITERABLE":

Aby sprawdzić, czy obiekt w JavaScript jest iterowalny, można użyć operatora typeof w połączeniu z operatorami logicznymi, lub sprawdzić, czy obiekt posiada metodę @@iterator. Obiekty iterowalne to takie, które mają zdefiniowaną metodę [Symbol.iterator], która pozwala na przechodzenie przez ich elementy. Sposoby sprawdzania:1. Użycie operatora typeof i operatorów logicznychMożna sprawdzić, czy obiekt ma protokół iterowalności (czyli metodę [Symbol.iterator]). javascript

function czyJestIterowalny(obj) {
  if (obj === null || typeof obj === 'undefined') {
    return false;
  }
  return typeof obj[Symbol.iterator] === 'function';
}

const tablica = [1, 2, 3];
const ciag = "tekst";
const obiekt = { a: 1 };

console.log(czyJestIterowalny(tablica)); // true
console.log(czyJestIterowalny(ciag));    // true
console.log(czyJestIterowalny(obiekt));  // false

2. Próba użycia pętli for...ofJeśli pętla for...of nie zgłosi błędu TypeError, oznacza to, że obiekt jest iterowalny. javascript

function czyJestIterowalny(obj) {
  try {
    // Próba iteracji, jeśli się uda, zwraca true
    for (const item of obj) {
      // Nie robimy nic, tylko sprawdzamy czy iteracja jest możliwa
    }
    return true;
  } catch (e) {
    // Jeśli wystąpi błąd, oznacza to, że nie jest iterowalny
    if (e instanceof TypeError) {
      return false;
    }
    throw e; // Inne błędy są propagowane
  }
}

const tablica = [1, 2, 3];
const ciag = "tekst";
const obiekt = { a: 1 };

console.log(czyJestIterowalny(tablica)); // true
console.log(czyJestIterowalny(ciag));    // true
console.log(czyJestIterowalny(obiekt));  // false

3. Użycie Object.prototype.propertyIsEnumerable()Ta metoda jest przydatna do sprawdzania, czy konkretna właściwość obiektu może być iterowana w pętli for...in. javascript

const obj = { a: 1 };
console.log(obj.propertyIsEnumerable('a')); // true
console.log(obj.propertyIsEnumerable('b')); // false