Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Breakpointy i krokowe wykonanie kodu

W poprzednim ćwiczeniu poznaliśmy konsolę przeglądarki i jej funkcje do logowania informacji. Jednak nawet najlepsze komunikaty w konsoli mogą być niewystarczające, gdy mamy do czynienia ze skomplikowanymi błędami w systemie zarządzania Parkiem Jurajskim. Co jeśli funkcja

validateDinosaurContainment()
zwraca nieprawidłowy wynik, ale nie wiemy dlaczego? Co jeśli algorytm śledzenia zachowań drapieżników zawiera subtelny błąd?

W takich przypadkach potrzebujemy możliwości wstrzymania wykonania kodu w określonym miejscu i przeanalizowania stanu aplikacji. Tutaj z pomocą przychodzą breakpointy i krokowe wykonanie kodu - jedne z najpotężniejszych technik debugowania aplikacji JavaScript.

Czym są breakpointy?

Breakpoint (punkt wstrzymania) to znacznik umieszczony w kodzie, który mówi przeglądarce, aby zatrzymała wykonanie kodu, gdy dotrze do tego miejsca. Pozwala to na dokładne zbadanie stanu aplikacji w wybranym momencie - możemy sprawdzić wartości zmiennych, obejrzeć stos wywołań i wykonać kod krok po kroku.

Ustawianie breakpointów w przeglądarce

Istnieje kilka sposobów ustawiania breakpointów w przeglądarce:

1. Breakpointy w Panelu Sources/Debugger

Najprostszym sposobem jest skorzystanie z panelu Sources (Chrome) lub Debugger (Firefox):

  1. Otwórz narzędzia deweloperskie (<kbd>F12</kbd> lub <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd>)
  2. Przejdź do zakładki "Sources" (Chrome) lub "Debugger" (Firefox)
  3. Znajdź i otwórz plik, w którym chcesz ustawić breakpoint
  4. Kliknij numer linii, w której chcesz zatrzymać wykonanie kodu
  5. W wybranym miejscu pojawi się niebieska strzałka lub znacznik, wskazując aktywny breakpoint

Na przykład, możemy chcieć zatrzymać wykonanie kodu w momencie, gdy sprawdzamy status ogrodzenia w Parku Jurajskim:

1function checkFenceStatus(sectorId) {
2  const sector = getSector(sectorId); // Ustawiamy breakpoint w tej linii
3
4  if (!sector) {
5    throw new Error(`Nieznany sektor: ${sectorId}`);
6  }
7
8  const fenceStatus = sector.fence.getStatus();
9
10  if (fenceStatus !== 'active') {
11    triggerAlarm(sectorId, `Awaria ogrodzenia: ${fenceStatus}`);
12  }
13
14  return fenceStatus;
15}

Po ustawieniu breakpointa, gdy funkcja

checkFenceStatus()
zostanie wywołana, przeglądarka zatrzyma wykonanie kodu w tej linii, a my będziemy mogli sprawdzić wartość
sectorId
i inne dostępne zmienne.

2. Programowe breakpointy z instrukcją debugger

Czasami wygodniejsze jest umieszczenie breakpointu bezpośrednio w kodzie, używając instrukcji

debugger
:

1function trackDinosaur(dinosaurId, location) {
2  const dinosaur = getDinosaurById(dinosaurId);
3
4  debugger; // Kod zatrzyma się tutaj, gdy funkcja zostanie wywołana
5
6  if (!dinosaur) {
7    console.error(`Nie znaleziono dinozaura o ID: ${dinosaurId}`);
8    return false;
9  }
10
11  updateDinosaurLocation(dinosaurId, location);
12  checkContainmentStatus(dinosaurId, location);
13
14  return true;
15}

Instrukcja

debugger
zostanie zignorowana, jeśli narzędzia deweloperskie nie są otwarte, ale gdy są aktywne, przeglądarka zatrzyma wykonanie kodu w tym miejscu.

3. Warunkowe breakpointy

Czasem chcemy zatrzymać wykonanie kodu tylko wtedy, gdy spełniony jest określony warunek. Na przykład, gdy śledzimy dinozaura o konkretnym ID lub gdy poziom zagrożenia przekracza określoną wartość:

Ustawianie w interfejsie przeglądarki

  1. Ustaw zwykły breakpoint, klikając numer linii
  2. Kliknij prawym przyciskiem myszy na breakpoint
  3. Wybierz opcję "Edit breakpoint" lub "Add condition"
  4. Wprowadź warunek, na przykład:
    dinosaurId === 'TRX-01'
    lub
    dangerLevel > 8

Programowe warunkowe zatrzymanie

Możemy również użyć konstrukcji warunkowej z instrukcją

debugger
:

1function monitorDinosaurBehavior(dinosaur) {
2  const behaviorPattern = analyzeBehavior(dinosaur);
3  const dangerLevel = assessThreatLevel(behaviorPattern);
4
5  // Zatrzymaj wykonanie tylko jeśli poziom zagrożenia jest wysoki
6  if (dangerLevel > 8) {
7    debugger; // Zatrzyma się tylko dla wysokiego poziomu zagrożenia
8
9    // Kod do obsługi niebezpiecznych zachowań
10    activateEmergencyProtocol(dinosaur.id, dangerLevel);
11  }
12
13  return {
14    dinosaurId: dinosaur.id,
15    behaviorPattern,
16    dangerLevel,
17    timestamp: new Date()
18  };
19}

Krokowe wykonanie kodu (Step Execution)

Gdy kod jest zatrzymany na breakpoincie, mamy kilka opcji krokowego wykonania, które pozwalają na kontrolowane przejście przez kod:

1. Step Over (Krok nad / F10)

Wykonuje aktualną linię kodu i zatrzymuje się na następnej linii w tej samej funkcji. Jeśli aktualna linia zawiera wywołanie funkcji, funkcja ta jest wykonywana w całości (nie zatrzymujemy się wewnątrz niej).

1function monitorParkSystems() {
2  checkSecuritySystems();     // Step Over wykona całą tę funkcję...
3  checkDinosaurContainment(); // ...i zatrzyma się na tej linii
4  checkVisitorSafety();
5}

2. Step Into (Krok do / F11)

Wykonuje aktualną linię kodu, ale jeśli linia zawiera wywołanie funkcji, debugger "wchodzi" do tej funkcji i zatrzymuje się na jej pierwszej linii.

1function monitorParkSystems() {
2  checkSecuritySystems();     // Step Into "wejdzie" do tej funkcji...
3  checkDinosaurContainment(); // ...zamiast przejść od razu tutaj
4  checkVisitorSafety();
5}
6
7function checkSecuritySystems() {
8  const fenceStatus = checkFences();   // ...i zatrzyma się tutaj
9  const cameraStatus = checkCameras();
10  const gateStatus = checkGates();
11
12  return { fenceStatus, cameraStatus, gateStatus };
13}

3. Step Out (Krok na zewnątrz / Shift+F11)

Wykonuje pozostałą część aktualnej funkcji i zatrzymuje się po wyjściu z funkcji, w miejscu jej wywołania.

1function checkSecuritySystems() {
2  const fenceStatus = checkFences();
3  const cameraStatus = checkCameras(); // Jeśli jesteśmy tutaj...
4  const gateStatus = checkGates();
5
6  return { fenceStatus, cameraStatus, gateStatus };
7}
8
9function monitorParkSystems() {
10  checkSecuritySystems();     // ...Step Out wykona resztę funkcji i zatrzyma się tutaj
11  checkDinosaurContainment();
12  checkVisitorSafety();
13}

4. Continue (Kontynuuj / F8)

Wznawia wykonanie kodu aż do napotkania następnego breakpointa lub zakończenia skryptu.

Inspekcja zmiennych i stanu

Gdy kod jest zatrzymany na breakpoincie, możemy szczegółowo zbadać stan aplikacji:

1. Panel Scope/Variables

Jeden z najważniejszych paneli podczas debugowania - pokazuje wszystkie dostępne zmienne w aktualnym zasięgu (scope):

  • Local - zmienne lokalne w aktualnej funkcji
  • Closure - zmienne z domknięć (funkcji nadrzędnych)
  • Global - zmienne globalne
  • this - wartość
    this
    w aktualnym kontekście

Na przykład, gdy debugujemy funkcję monitorującą zachowanie dinozaurów:

1function analyzeDinosaurBehavior(dinosaur, environmentalFactors) {
2  const { species, previousBehaviors, lastFeeding } = dinosaur;
3  const { weather, timeOfDay, nearbyDinosaurs } = environmentalFactors;
4
5  const timeSinceFeeding = Date.now() - lastFeeding;
6  const isHungry = timeSinceFeeding > getSpeciesFeedingThreshold(species);
7
8  debugger; // zatrzymaj się tutaj
9
10  // W tym momencie w panelu Scope zobaczymy:
11  // - Local: dinosaur, environmentalFactors, species, previousBehaviors, lastFeeding,
12  //          weather, timeOfDay, nearbyDinosaurs, timeSinceFeeding, isHungry
13  // - Global: globalne obiekty i funkcje
14
15  const behaviorPrediction = predictBehavior(
16    species,
17    previousBehaviors,
18    isHungry,
19    weather,
20    timeOfDay,
21    nearbyDinosaurs
22  );
23
24  return {
25    dinosaurId: dinosaur.id,
26    prediction: behaviorPrediction,
27    riskLevel: assessRiskLevel(behaviorPrediction),
28    isHungry,
29    timestamp: new Date()
30  };
31}

2. Watch Expressions (Wyrażenia obserwowane)

Panel Watch pozwala na dodanie wyrażeń, które chcemy monitorować podczas debugowania. Jest to szczególnie przydatne dla złożonych wyrażeń lub wartości, które nie są bezpośrednio widoczne w panelu Scope:

1// Możemy dodać wyrażenia obserwowane takie jak:
2dinosaur.species === 'Velociraptor'
3getDistanceFromFence(dinosaur.location)
4isInDangerZone(dinosaur.location, restrictedSectors)

3. Konsola w trybie debugowania

W trakcie zatrzymania na breakpoincie, konsola działa w kontekście aktualnej pozycji w kodzie. Możemy więc wykonywać wyrażenia, które mają dostęp do wszystkich zmiennych widocznych w bieżącym zasięgu:

1// W konsoli możemy wpisać:
2dinosaur.species
3timeSinceFeeding / (1000 * 60 * 60) // Konwersja milisekund na godziny
4nearbyDinosaurs.filter(d => d.species === 'Velociraptor')

Jest to potężny sposób na interaktywne badanie stanu aplikacji i wykonywanie eksperymentów bez modyfikowania kodu.

Praktyczne techniki debugowania dla Parku Jurajskiego

1. Debugowanie asynchronicznego kodu

Systemy w Parku Jurajskim często wykorzystują asynchroniczne operacje - pobieranie danych z czujników, komunikacja z systemami bezpieczeństwa, etc. Debugowanie takiego kodu wymaga specjalnych technik:

1async function monitorsEnvironmentalSensors() {
2  try {
3    console.log("Rozpoczęcie monitorowania czujników...");
4
5    // Asynchroniczne pobranie danych z czujników
6    const temperatureData = await fetchSensorData('temperature');
7    debugger; // Breakpoint zatrzyma się po otrzymaniu odpowiedzi
8
9    const humidityData = await fetchSensorData('humidity');
10    const windData = await fetchSensorData('wind');
11
12    // Analiza danych środowiskowych
13    const environmentalRisk = analyzeEnvironmentalRisk(
14      temperatureData,
15      humidityData,
16      windData
17    );
18
19    if (environmentalRisk.level === 'high') {
20      notifyParkManagement(
21        `Wykryto wysokie ryzyko środowiskowe: ${environmentalRisk.reason}`
22      );
23    }
24
25    return environmentalRisk;
26  } catch (error) {
27    console.error("Błąd monitorowania czujników:", error);
28    triggerBackupSystems();
29    return { level: 'unknown', reason: 'error' };
30  }
31}

W narzędziach deweloperskich nowoczesnych przeglądarek, breakpointy działają poprawnie z asynchronicznymi funkcjami - wykonanie zatrzyma się w punkcie

debugger
, nawet po operacjach
await
.

2. Zatrzymywanie na wyjątkach (Exceptions)

W złożonym systemie jakim jest Park Jurajski, błędy mogą pojawiać się w nieoczekiwanych miejscach. Zamiast ręcznie ustawiać breakpointy, możemy skonfigurować debugger, aby zatrzymywał się, gdy wystąpi wyjątek:

  1. Otwórz panel Sources/Debugger
  2. Znajdź sekcję "Breakpoints" (lub podobną)
  3. Zaznacz opcję "Pause on exceptions" (Zatrzymaj na wyjątkach)
  4. Opcjonalnie: zaznacz "Pause on caught exceptions" (Zatrzymaj na obsłużonych wyjątkach)

Teraz przeglądarka zatrzyma wykonanie kodu, gdy wystąpi jakikolwiek nieobsłużony wyjątek, co znacznie ułatwia identyfikację źródła problemów.

3. Mapowanie źródeł (Source Maps)

W produkcyjnych aplikacjach kod JavaScript jest często minifikowany i bundlowany, co utrudnia debugowanie. Mapowanie źródeł pozwala przeglądarkom pokazywać i debugować oryginalny kod źródłowy, nawet gdy wykonywany jest kod zminifikowany:

1// webpack.config.js
2module.exports = {
3  //
4  devtool: 'source-map', // Generuje mapy źródeł dla wszystkich plików
5  //
6};

Dla Parku Jurajskiego, gdzie bezpieczeństwo jest priorytetem, to kluczowa funkcja umożliwiająca szybkie debugowanie produkcyjnych problemów.

4. Logowanie punktów kontrolnych

Czasami, zwłaszcza w złożonych systemach, warto łączyć breakpointy z logowaniem, aby śledzić wykonanie kodu:

1function monitorDinosaurMovement(dinosaurId) {
2  const dinosaur = getDinosaurById(dinosaurId);
3  console.log(`[Checkpoint 1] Rozpoczęcie monitorowania ${dinosaur.name}`);
4
5  if (!dinosaur.tracker.isActive) {
6    console.warn(`[Checkpoint 2A] Nieaktywny tracker dla ${dinosaur.name}`);
7    activateTracker(dinosaurId);
8    debugger; // Zatrzymaj się tylko, gdy tracker był nieaktywny
9  } else {
10    console.log(`[Checkpoint 2B] Tracker aktywny dla ${dinosaur.name}`);
11  }
12
13  const location = getLastKnownLocation(dinosaurId);
14  console.log(`[Checkpoint 3] Ostatnia lokalizacja: ${JSON.stringify(location)}`);
15
16  // Różne ścieżki bazujące na lokalizacji
17  if (isInRestrictedZone(location)) {
18    console.error(`[Checkpoint 4A] Dinozaur w strefie ograniczonej!`);
19    debugger; // Zawsze zatrzymaj się w przypadku naruszenia strefy
20    triggerContainmentProtocol(dinosaurId, location);
21  } else if (isApproachingFence(location)) {
22    console.warn(`[Checkpoint 4B] Dinozaur zbliża się do ogrodzenia`);
23    increaseSurveillance(dinosaurId);
24  } else {
25    console.log(`[Checkpoint 4C] Dinozaur w normalnej strefie`);
26  }
27
28  return {
29    dinosaurId,
30    location,
31    status: getCurrentStatus(dinosaurId)
32  };
33}

Taki wzorzec "checkpointów" pomaga śledzić przepływ wykonania i szybko identyfikować, która ścieżka kodu została wykonana, nawet gdy nie korzystamy aktywnie z debuggera.

Debugowanie w praktyce - analiza przypadku

Przeanalizujmy praktyczny przykład debugowania problemu w systemie monitorowania dinozaurów:

1class DinosaurMonitoringSystem {
2  constructor() {
3    this.activeMonitors = new Map();
4    this.alertLevel = 'normal';
5    this.lastScanTime = null;
6  }
7
8  startMonitoring(dinosaurId) {
9    if (this.activeMonitors.has(dinosaurId)) {
10      console.log(`Monitoring dla dinozaura ${dinosaurId} już aktywny`);
11      return false;
12    }
13
14    const dinosaur = getDinosaurById(dinosaurId);
15    if (!dinosaur) {
16      console.error(`Nie znaleziono dinozaura o ID: ${dinosaurId}`);
17      return false;
18    }
19
20    // Tworzymy nowy monitor i dodajemy do aktywnych
21    const monitor = {
22      dinosaurId,
23      species: dinosaur.species,
24      startTime: Date.now(),
25      lastUpdate: Date.now(),
26      status: 'active',
27      readings: [],
28      checkInterval: getSpeciesCheckInterval(dinosaur.species)
29    };
30
31    this.activeMonitors.set(dinosaurId, monitor);
32
33    // Uruchamiamy interwał do regularnego sprawdzania
34    const intervalId = setInterval(() => {
35      this.checkDinosaurStatus(dinosaurId);
36    }, monitor.checkInterval);
37
38    monitor.intervalId = intervalId;
39
40    console.log(`Rozpoczęto monitoring dinozaura ${dinosaurId} (${dinosaur.species})`);
41    return true;
42  }
43
44  checkDinosaurStatus(dinosaurId) {
45    const monitor = this.activeMonitors.get(dinosaurId);
46
47    if (!monitor) {
48      console.error(`Brak aktywnego monitoringu dla dinozaura ${dinosaurId}`);
49      return null;
50    }
51
52    try {
53      // Pobieranie aktualnych danych
54      const dinosaur = getDinosaurById(dinosaurId);
55      const location = getLastKnownLocation(dinosaurId);
56      const vitalSigns = getVitalSigns(dinosaurId);
57      const behavior = analyzeBehavior(dinosaurId, location);
58
59      // BŁĄD: Nieprawidłowe obliczenie statusu zdrowia
60      const healthStatus = calculateHealthStatus(vitalSigns);
61
62      // BŁĄD: Nie sprawdzamy czy location istnieje!
63      const inContainment = isInContainmentZone(location.coordinates);
64
65      const reading = {
66        timestamp: Date.now(),
67        location,
68        vitalSigns,
69        behavior,
70        healthStatus,
71        inContainment
72      };
73
74      // Aktualizujemy monitor
75      monitor.readings.push(reading);
76      monitor.lastUpdate = reading.timestamp;
77      monitor.locationStatus = inContainment ? 'contained' : 'breach';
78      monitor.healthStatus = healthStatus;
79
80      // Sprawdzamy poziom zagrożenia
81      this.assessThreatLevel();
82
83      return reading;
84    } catch (error) {
85      console.error(`Błąd sprawdzania statusu dinozaura ${dinosaurId}:`, error);
86      monitor.lastError = {
87        timestamp: Date.now(),
88        message: error.message
89      };
90
91      return null;
92    }
93  }
94
95  stopMonitoring(dinosaurId) {
96    const monitor = this.activeMonitors.get(dinosaurId);
97
98    if (!monitor) {
99      console.warn(`Brak aktywnego monitoringu dla dinozaura ${dinosaurId}`);
100      return false;
101    }
102
103    // Zatrzymujemy interwał
104    clearInterval(monitor.intervalId);
105
106    // Usuwamy z aktywnych monitorów
107    this.activeMonitors.delete(dinosaurId);
108
109    console.log(`Zatrzymano monitoring dinozaura ${dinosaurId}`);
110    return true;
111  }
112
113  assessThreatLevel() {
114    // Logika oceny poziomu zagrożenia na podstawie wszystkich monitorów
115    let highestThreat = 'normal';
116
117    for (const [id, monitor] of this.activeMonitors.entries()) {
118      if (!monitor.locationStatus || monitor.locationStatus === 'breach') {
119        highestThreat = 'critical';
120        break;
121      }
122
123      if (monitor.healthStatus === 'agitated' && highestThreat !== 'critical') {
124        highestThreat = 'elevated';
125      }
126    }
127
128    // Aktualizujemy poziom alertu
129    if (highestThreat !== this.alertLevel) {
130      console.log(`Zmiana poziomu alertu: ${this.alertLevel} -> ${highestThreat}`);
131      this.alertLevel = highestThreat;
132
133      // Powiadomienie o zmianie poziomu alertu
134      notifyAlertLevelChange(this.alertLevel);
135    }
136
137    return this.alertLevel;
138  }
139}

Krok 1: Zidentyfikowanie problemu

Podczas testowania systemu monitorowania w Parku Jurajskim, odkrywamy, że system nieoczekiwanie zgłasza poziom alertu 'critical' dla niektórych dinozaurów, mimo że są one bezpiecznie w swoich wybiegach.

Krok 2: Ustawienie breakpointów

Ustawiamy breakpoint w metodzie

checkDinosaurStatus
, aby zbadać, co się dzieje:

1checkDinosaurStatus(dinosaurId) {
2  const monitor = this.activeMonitors.get(dinosaurId);
3
4  if (!monitor) {
5    console.error(`Brak aktywnego monitoringu dla dinozaura ${dinosaurId}`);
6    return null;
7  }
8
9  debugger; // Ustawiamy breakpoint tutaj, aby zbadać wszystkie zmienne
10
11  try {
12    // ...reszta kodu
13  }
14}

Krok 3: Krokowe wykonanie i analiza

  1. Wykonujemy kod krok po kroku używając "Step Into" i "Step Over"
  2. Dochodzimy do linii, gdzie sprawdzany jest status lokalizacji:
    1// BŁĄD: Nie sprawdzamy czy location istnieje!
    2const inContainment = isInContainmentZone(location.coordinates);
  3. Sprawdzamy wartość zmiennej
    location
    w panelu Scope i widzimy, że jest
    null
  4. Zidentyfikowany problem: Funkcja
    getLastKnownLocation
    czasem zwraca
    null
    , gdy nie może znaleźć lokalizacji, a kod nie sprawdza tego przypadku przed próbą dostępu do
    location.coordinates

Krok 4: Poprawa błędu

Modyfikujemy kod, aby obsługiwał przypadek, gdy

location
jest
null
:

1// Bezpieczniejszy kod z weryfikacją
2const location = getLastKnownLocation(dinosaurId);
3const inContainment = location && location.coordinates
4  ? isInContainmentZone(location.coordinates)
5  : false; // Jeśli nie znamy lokalizacji, zakładamy najgorszy scenariusz

Krok 5: Weryfikacja rozwiązania

Po naprawieniu błędu, usuwamy breakpoint i testujemy system ponownie. Tym razem działa poprawnie, nie zgłaszając fałszywych alarmów.

Dobre praktyki debugowania

  1. Używaj znaczących nazw zmiennych - ułatwia to identyfikację zmiennych podczas debugowania
  2. Dodawaj dokumentację do funkcji - komentarze JSDoc pomagają zrozumieć cel funkcji i oczekiwane wartości
  3. Strukturyzuj kod logicznie - przejrzysty kod jest łatwiejszy do debugowania
  4. Używaj defensywnego programowania - sprawdzaj dane wejściowe i obsługuj przypadki brzegowe
  5. Twórz małe, dedykowane funkcje - łatwiej debugować funkcje o pojedynczej odpowiedzialności
  6. Używaj try-catch dla potencjalnie problematycznych części kodu - pomaga to zlokalizować źródło błędów
  7. Unikaj modyfikacji zmiennych debugowanych - może to zmienić zachowanie programu

Podsumowanie

Breakpointy i krokowe wykonanie kodu to nieocenione narzędzia w arsenale każdego programisty pracującego przy systemach Parku Jurajskiego. Pozwalają na:

  • Zatrzymanie wykonania kodu w kluczowych momentach
  • Zbadanie stanu aplikacji, wartości zmiennych i kontekstu wykonania
  • Wykonanie kodu krok po kroku, aby zrozumieć jego przepływ
  • Identyfikację źródła błędów, zwłaszcza w złożonych systemach
  • Weryfikację poprawności działania funkcji i modułów

Pamiętaj, że w Parku Jurajskim, umiejętność szybkiego znalezienia i naprawienia błędów może być kwestią życia lub śmierci - zarówno kodu, jak i (w prawdziwym parku) odwiedzających!

Przejdź do CodeWorlds