5 Der Browser als Laufzeitumgebung

Der Webbrowser ist die ursprüngliche und nach wie vor wichtigste Laufzeitumgebung für JavaScript. Um JavaScript-Code effektiv zu entwickeln, ist ein tiefes Verständnis der Browser-Umgebung unerlässlich – von der JavaScript-Engine über das Document Object Model (DOM) bis hin zu den zahlreichen Web-APIs.

5.1 JavaScript-Engines

Eine JavaScript-Engine ist der Kern jedes Browsers, der für die Ausführung von JavaScript-Code verantwortlich ist. Die wichtigsten Engines sind:

Diese Engines übersetzen JavaScript-Code in Maschinencode und führen ihn aus. Der Prozess umfasst typischerweise:

  1. Parsing: Umwandlung des Textcodes in einen Abstract Syntax Tree (AST)
  2. Kompilierung: Umwandlung des AST in Bytecode
  3. Just-in-Time (JIT) Kompilierung: Optimierung von häufig ausgeführtem Code in hochoptimierten Maschinencode
  4. Ausführung: Ausführung des kompilierten Codes
  5. Garbage Collection: Automatische Speicherbereinigung
// Dieses einfache Beispiel durchläuft alle oben genannten Phasen
function add(a, b) {
  return a + b;
}

// Bei wiederholten Aufrufen kann die JIT-Kompilierung einsetzen
for (let i = 0; i < 10000; i++) {
  add(i, i + 1);
}

Der Browser-Prozess ist in der Regel multi-threaded, wobei JavaScript auf dem Hauptthread (UI-Thread) ausgeführt wird. Dies erklärt, warum rechenintensive JavaScript-Operationen die Benutzeroberfläche blockieren können.

5.2 Browser-Objektmodell (BOM)

Das Browser-Objektmodell bietet JavaScript Zugriff auf die Funktionalitäten des Browsers selbst und ist nicht standardisiert. Das globale window-Objekt ist der Einstiegspunkt zum BOM und enthält unter anderem:

5.2.1 Window-Objekt

Das window-Objekt ist das globale Objekt im Browser und bietet Zugriff auf:

// Fenstergröße und -position
console.log(window.innerWidth, window.innerHeight);
window.moveTo(100, 100);
window.resizeTo(800, 600);

// Browserfunktionen
window.alert('Hinweis');
window.confirm('Möchten Sie fortfahren?');
window.prompt('Bitte geben Sie Ihren Namen ein');

// Timing-Funktionen
const timerId = window.setTimeout(() => {
  console.log('Nach 1 Sekunde ausgeführt');
}, 1000);
window.clearTimeout(timerId);

const intervalId = window.setInterval(() => {
  console.log('Wird alle 2 Sekunden ausgeführt');
}, 2000);
window.clearInterval(intervalId);

5.2.2 Location-Objekt

Mit window.location lässt sich die URL des Browsers manipulieren:

// Aktuelle URL-Komponenten
console.log(location.href);     // Vollständige URL
console.log(location.protocol); // "http:" oder "https:"
console.log(location.host);     // Hostname mit Port
console.log(location.pathname); // Pfadteil der URL
console.log(location.search);   // Query-String
console.log(location.hash);     // URL-Fragment

// Navigation auslösen
location.href = 'https://example.com';         // Vollständige Navigation
location.assign('https://example.com');        // Äquivalent zu href-Zuweisung
location.replace('https://example.com');       // Ersetzt aktuellen Eintrag im Verlauf
location.reload();                             // Seite neu laden

5.2.3 History-Objekt

window.history ermöglicht die Navigation im Browser-Verlauf:

// Im Verlauf navigieren
history.back();       // Eine Seite zurück
history.forward();    // Eine Seite vor
history.go(-2);       // Zwei Seiten zurück

// History API für SPAs (Single Page Applications)
history.pushState({page: 1}, "Titel", "/neuer-pfad");  // Verlaufseintrag hinzufügen
history.replaceState({page: 1}, "Titel", "/pfad");     // Aktuellen Eintrag ersetzen

// Bei Navigation zurück/vor wird das popstate-Event ausgelöst
window.addEventListener('popstate', (event) => {
  console.log('Navigation im Verlauf', event.state);
});

window.navigator bietet Informationen über den Browser und das System:

// Browser- und Systeminformationen
console.log(navigator.userAgent);        // Browser-Identifikation
console.log(navigator.platform);         // Betriebssystem
console.log(navigator.language);         // Bevorzugte Sprache
console.log(navigator.onLine);           // Online-Status

// Zugriff auf Hardware (mit Berechtigungen)
navigator.geolocation.getCurrentPosition((position) => {
  console.log(position.coords.latitude, position.coords.longitude);
});

5.3 Document Object Model (DOM)

Das DOM ist eine programmatische Repräsentation der HTML-Struktur einer Webseite. Es ermöglicht JavaScript, auf die Struktur, den Inhalt und das Styling der Webseite zuzugreifen und diese zu manipulieren.

5.3.1 DOM-Struktur

Das DOM stellt ein HTML-Dokument als Baumstruktur dar, beginnend mit dem document-Objekt:

// Zugriff auf Dokumentinformationen
console.log(document.title);           // Titel der Seite
console.log(document.URL);             // URL des Dokuments
console.log(document.doctype);         // Doctype-Information
console.log(document.documentElement); // Das <html>-Element

5.3.2 DOM-Navigation

Nodes im DOM-Baum können auf verschiedene Weise durchlaufen werden:

// Grundlegende Navigation
const body = document.body;
const firstChild = body.firstChild;
const lastChild = body.lastChild;
const parent = firstChild.parentNode;
const nextSibling = firstChild.nextSibling;
const previousSibling = lastChild.previousSibling;

// Nur Element-Nodes
const firstElement = body.firstElementChild;
const lastElement = body.lastElementChild;
const nextElement = firstElement.nextElementSibling;
const previousElement = lastElement.previousElementSibling;
const parentElement = firstElement.parentElement;

5.3.3 DOM-Manipulation

JavaScript kann den DOM-Baum dynamisch verändern:

// Elemente erstellen
const div = document.createElement('div');
div.textContent = 'Neues Element';
div.className = 'container';
div.id = 'main-container';

// Attribute manipulieren
div.setAttribute('data-created', 'dynamically');
const hasAttr = div.hasAttribute('data-created');
const attrValue = div.getAttribute('data-created');
div.removeAttribute('data-created');

// Elemente in den DOM einfügen
document.body.appendChild(div);              // Als letztes Kind anfügen
document.body.insertBefore(div, firstChild); // Vor einem bestimmten Element einfügen

// Moderne Methoden (bessere Browser-Unterstützung prüfen)
parentElement.append(div);                   // Als letztes Kind anfügen
parentElement.prepend(div);                  // Als erstes Kind anfügen
existingElement.before(div);                 // Vor einem Element einfügen
existingElement.after(div);                  // Nach einem Element einfügen
existingElement.replaceWith(div);            // Element ersetzen

// Elemente entfernen
element.remove();                            // Moderner Ansatz
parentElement.removeChild(element);          // Älterer, universeller Ansatz

// Inhalt manipulieren
element.textContent = 'Nur Text';            // Nur Text, ohne HTML-Parsing
element.innerHTML = '<strong>HTML</strong>'; // Mit HTML-Parsing (potenziell unsicher!)
element.innerText = 'Sichtbarer Text';       // Nur sichtbarer Text (vermeiden)

5.3.4 DOM-Elementauswahl

Es gibt verschiedene Methoden, um Elemente im DOM zu finden:

// Direkte Selektoren (liefern einzelne Elemente)
const elementById = document.getElementById('main-heading');
const firstByTag = document.querySelector('h1');
const firstByClass = document.querySelector('.container');
const firstByAttribute = document.querySelector('[data-role="button"]');

// Selektoren für mehrere Elemente (liefern NodeList oder HTMLCollection)
const allParagraphs = document.getElementsByTagName('p');       // Live HTMLCollection
const allButtons = document.getElementsByClassName('btn');      // Live HTMLCollection
const allLinks = document.querySelectorAll('a.external-link');  // Statische NodeList

// Live vs. statisch ist ein wichtiger Unterschied:
const paragraphs = document.getElementsByTagName('p');  // Live
const paragraphsQuery = document.querySelectorAll('p'); // Statisch

// Wenn ein neuer Paragraph hinzugefügt wird:
document.body.appendChild(document.createElement('p'));

console.log(paragraphs.length);        // Erhöht um 1 (live)
console.log(paragraphsQuery.length);   // Unverändert (statisch)

5.4 Event-Handling

Der Browser ist ereignisgesteuert. JavaScript kann auf Benutzerinteraktionen und andere Events reagieren:

5.4.1 Event-Listener hinzufügen und entfernen

function handleClick(event) {
  console.log('Button wurde geklickt!', event);
}

// Event-Listener hinzufügen
button.addEventListener('click', handleClick);

// Mit zusätzlichen Optionen
button.addEventListener('click', handleClick, {
  once: true,       // Listener wird nach einmaliger Ausführung entfernt
  capture: true,    // Event wird in der Capturing-Phase behandelt
  passive: true     // Performance-Optimierung (verhindert preventDefault)
});

// Event-Listener entfernen
button.removeEventListener('click', handleClick);

5.4.2 Event-Objekt

Das Event-Objekt enthält detaillierte Informationen über das ausgelöste Event:

element.addEventListener('click', (event) => {
  console.log(event.type);          // Art des Events ("click")
  console.log(event.target);        // Element, das das Event ausgelöst hat
  console.log(event.currentTarget); // Element, an dem der Listener registriert ist
  console.log(event.clientX, event.clientY); // Mausposition
  
  // Standardverhalten verhindern
  event.preventDefault();
  
  // Event-Propagation stoppen
  event.stopPropagation();
});

5.4.3 Event-Propagation: Bubbling und Capturing

Events durchlaufen drei Phasen:

  1. Capturing-Phase: Das Event wird vom Wurzelelement zum Zielelement weitergeleitet
  2. Target-Phase: Das Event erreicht das Zielelement
  3. Bubbling-Phase: Das Event “blubbert” vom Zielelement zurück zum Wurzelelement
// HTML-Struktur:
// <div id="outer">
//   <div id="middle">
//     <button id="inner">Klick mich</button>
//   </div>
// </div>

document.getElementById('outer').addEventListener('click', (e) => {
  console.log('Outer div geklickt (Bubbling)');
}, false); // false = Bubbling (Standard)

document.getElementById('middle').addEventListener('click', (e) => {
  console.log('Middle div geklickt (Bubbling)');
}, false);

document.getElementById('inner').addEventListener('click', (e) => {
  console.log('Button geklickt (Bubbling)');
}, false);

// Capturing-Phase-Listener
document.getElementById('outer').addEventListener('click', (e) => {
  console.log('Outer div geklickt (Capturing)');
}, true); // true = Capturing

// Bei Klick auf den Button ist die Ausgabereihenfolge:
// 1. "Outer div geklickt (Capturing)" - Capturing-Phase
// 2. "Button geklickt (Bubbling)" - Target erreicht
// 3. "Middle div geklickt (Bubbling)" - Bubbling-Phase
// 4. "Outer div geklickt (Bubbling)" - Bubbling-Phase

5.4.4 Event-Delegation

Event-Delegation ist ein leistungsfähiges Muster, bei dem Events an übergeordneten Elementen behandelt werden:

// Statt vieler einzelner Listener auf jedem Listenelement...
document.querySelector('ul').addEventListener('click', (event) => {
  // Prüfen, ob ein Listenelement geklickt wurde
  if (event.target.tagName === 'LI') {
    console.log('Listenelement geklickt:', event.target.textContent);
  }
});

Dies ist besonders nützlich für dynamisch erzeugte Elemente und spart Ressourcen.

5.5 Web APIs

Moderne Browser bieten zahlreiche APIs für erweiterte Funktionalitäten:

5.5.1 Fetch API

Die Fetch API bietet eine moderne Schnittstelle für HTTP-Anfragen:

// GET-Anfrage
fetch('https://api.example.com/data')
  .then(response => {
    if (!response.ok) {
      throw new Error('Netzwerkantwort war nicht ok');
    }
    return response.json(); // Oder response.text(), response.blob() etc.
  })
  .then(data => {
    console.log('Daten empfangen:', data);
  })
  .catch(error => {
    console.error('Fetch-Fehler:', error);
  });

// POST-Anfrage mit zusätzlichen Optionen
fetch('https://api.example.com/submit', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Max Mustermann',
    email: 'max@example.com'
  })
})
.then(response => response.json())
.then(data => console.log('Antwort:', data));

// Mit async/await (wird in späteren Kapiteln detaillierter behandelt)
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) throw new Error('Netzwerkfehler');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Fehler beim Abrufen der Daten:', error);
  }
}

5.5.2 Web Storage API

Für die clientseitige Datenspeicherung:

// localStorage (persistent)
localStorage.setItem('username', 'Max');
const username = localStorage.getItem('username');
localStorage.removeItem('username');
localStorage.clear(); // Alles löschen

// sessionStorage (nur für die Dauer der Sitzung)
sessionStorage.setItem('temporaryData', JSON.stringify({id: 123}));
const data = JSON.parse(sessionStorage.getItem('temporaryData'));

5.5.3 Weitere wichtige Web APIs

Diese APIs werden in späteren Kapiteln detaillierter behandelt.

5.6 Sicherheitsmodell des Browsers

JavaScript im Browser unterliegt strengen Sicherheitsrichtlinien:

5.6.1 Same-Origin-Policy (SOP)

Die SOP schränkt den Zugriff auf Ressourcen aus anderen Herkunftsquellen ein:

5.6.2 Cross-Origin Resource Sharing (CORS)

CORS ist ein Mechanismus, der kontrollierte Cross-Origin-Anfragen ermöglicht:

// Server muss entsprechende CORS-Header senden
fetch('https://api.other-domain.com/data', {
  // Credentials nur bei expliziter Erlaubnis durch den Server
  credentials: 'include'
});

5.6.3 Content Security Policy (CSP)

CSP ist eine zusätzliche Sicherheitsebene, die XSS-Angriffe einschränkt:

<!-- Via HTTP-Header oder Meta-Tag -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' trusted-scripts.com">

5.7 Rendering und Performance

JavaScript kann die Rendering-Performance erheblich beeinflussen:

5.7.1 Kritischer Rendering-Pfad

Der Browser führt mehrere Schritte durch, um eine Seite darzustellen:

  1. DOM-Konstruktion aus HTML
  2. CSSOM-Konstruktion aus CSS
  3. Render-Tree-Erstellung (Kombination aus DOM und CSSOM)
  4. Layout-Berechnung (Größe und Position)
  5. Painting (Füllen von Pixeln)

JavaScript kann diesen Prozess in jeder Phase unterbrechen oder ändern.

5.7.2 Reflow und Repaint

// Ineffizient: Verursacht mehrere Reflows
element.style.width = '100px';
element.style.height = '200px';
element.style.margin = '10px';

// Effizienter: Ein einziger Reflow
element.style.cssText = 'width: 100px; height: 200px; margin: 10px;';
// Oder
element.className = 'new-style';

5.7.3 requestAnimationFrame

Für flüssige Animationen und visuelle Updates:

function animate() {
  // Visuelle Aktualisierungen hier
  element.style.transform = `translateX(${position}px)`;
  position += 5;
  
  // Nächsten Frame anfordern
  requestAnimationFrame(animate);
}

// Animation starten
requestAnimationFrame(animate);

5.8 Offline-Fähigkeit und Progressive Web Apps

Moderne Webanwendungen können auch offline funktionieren:

5.8.1 Service Workers

Service Workers sind JavaScript-Worker, die als Proxy zwischen Browser und Netzwerk agieren:

// Service Worker registrieren
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      console.log('Service Worker registriert:', registration.scope);
    })
    .catch(error => {
      console.error('Service Worker Registrierung fehlgeschlagen:', error);
    });
}

// In der sw.js-Datei:
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('v1').then(cache => {
      return cache.addAll([
        '/',
        '/index.html',
        '/styles.css',
        '/app.js'
      ]);
    })
  );
});

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      return response || fetch(event.request);
    })
  );
});

Diese Themen werden im Kapitel zu Progressive Web Apps vertieft.