Was ist das DOM?
Wenn der Browser dein HTML liest, speichert er dies nicht als einfachen Textstring. Er erstellt einen Baum aus Objekten, das sogenannte Document Object Model. Jedes Element wird zu einem Node mit Eigenschaften und Methoden, jedes Attribut ist zugänglich und jedes Textstück hat seinen festen Platz. Das DOM ist dieser Baum, und er ist live: Ändere ihn, und die Seite aktualisiert sich.
Das ist der Grund, warum JavaScript und HTML so eng miteinander verknüpft sind. Dein HTML beschreibt die Struktur, der Browser wandelt diese in Objekte um und JavaScript manipuliert diese Objekte. Das document-Objekt ist dabei der Einstiegspunkt in den gesamten Baum.
Der Node-Baum
Das DOM ist hierarchisch aufgebaut. document ist die Wurzel (Root), <html> ist dessen Kind und alles Weitere hängt in einer Eltern-Kind-Beziehung darunter. Ein Node kann viele Kinder haben, aber nur einen einzigen Eltern-Node.
document
└── html
├── head
│ └── title
└── body
├── h1
└── p
Es gibt verschiedene Arten von Nodes. Element-Nodes repräsentieren Tags. Text-Nodes enthalten die Zeichen innerhalb von Elementen. Comment-Nodes repräsentieren Kommentare. Meistens arbeitet man mit Elementen, aber das Wissen um die Existenz von Text-Nodes erklärt unerwartete Leerzeichen und warum childNodes mehr enthalten kann, als man eigentlich erwartet.
Elemente auswählen
Bevor Sie etwas ändern können, müssen Sie es zuerst finden. Das moderne Standardwerkzeug ist querySelector, welches jeden CSS-Selektor akzeptiert.
// select.js
const title = document.querySelector("h1");
const firstCard = document.querySelector(".card");
const allLinks = document.querySelectorAll("a[target='_blank']");
const byId = document.getElementById("main");
allLinks.forEach((link) => link.classList.add("external"));
querySelector gibt das erste passende Element oder null zurück. querySelectorAll gibt eine statische NodeList aller Treffer zurück, die forEach unterstützt, sich jedoch nicht aktualisiert, wenn sich der DOM ändert. Die älteren Methoden getElementById, getElementsByClassName und getElementsByTagName funktionieren weiterhin; die letzten beiden geben live Collections zurück, die sich automatisch aktualisieren.
Inhalte lesen und schreiben
Sobald Sie ein Element haben, können Sie dessen Inhalt lesen oder ersetzen.
// content.js
const heading = document.querySelector("#title");
heading.textContent; // read the visible text
heading.textContent = "Hello"; // replace it, escaped as text
heading.innerHTML = "<em>Hi</em>"; // parses HTML
Verwenden Sie textContent immer dann, wenn der Wert von einem Benutzer, einer API oder aus einer anderen Quelle stammt, die Sie nicht vollständig kontrollieren. Dabei wird alles als einfacher Text behandelt, sodass kein Markup injiziert werden kann. innerHTML parst einen String als HTML, was für vertrauenswürdige Templates praktisch ist, aber bei nicht vertrauenswürdigen Eingaben ein ernsthaftes Cross-Site Scripting-Risiko darstellt. Wenn Sie Markup aus Daten generieren müssen, ist es besser, Elemente zu erstellen und für jedes einzelne textContent zu setzen.
Attribute, Klassen und Styles
Attribute befinden sich direkt am Element und können unmittelbar ausgelesen oder geändert werden.
// attrs.js
const link = document.querySelector("a");
link.getAttribute("href");
link.setAttribute("href", "/docs");
link.dataset.userId = "42"; // data-user-id="42"
link.classList.add("active");
link.classList.toggle("open");
link.classList.remove("hidden");
link.style.color = "tomato";
classList ist der saubere Weg, um Klassen zu verwalten – so vermeidet man String-Konkatenation und versehentliche doppelte Klassen. Die style-Property setzt Inline-Styles für jede Eigenschaft einzeln; für thematische Anpassungen ist es besser, eine Klasse zu toggeln und die Entscheidung CSS zu überlassen.
Knoten erstellen, einfügen und entfernen
Sie erstellen neue Knoten mit document.createElement und platzieren diese mithilfe der Einfügemethoden.
// build.js
const item = document.createElement("li");
item.textContent = "New task";
item.classList.add("task");
const list = document.querySelector("#todos");
list.append(item); // add at the end
list.prepend(item); // add at the start
item.remove(); // detach from the tree
append akzeptiert mehrere Knoten und sogar Strings, während insertBefore eine präzise Platzierung ermöglicht. Wenn Sie viele Knoten einfügen müssen, erstellen Sie diese außerhalb des Dokuments und hängen Sie sie einmalig an:
// fragment.js
const fragment = document.createDocumentFragment();
for (const name of names) {
const li = document.createElement("li");
li.textContent = name;
fragment.append(li);
}
list.append(fragment);
Ein DocumentFragment ist ein leichtgewichtiger Container, der nicht Teil der Seite ist. Durch das Anhängen werden alle seine Kinder in einem einzigen Vorgang verschoben, was deutlich weniger Layout-Arbeit auslöst, als wenn sie einzeln hinzugefügt würden.
Auf Events hören
Events sind die Art und Weise, wie dein Code auf den Nutzer und den Browser reagiert. Füge einen Listener mit addEventListener hinzu.
// events.js
const button = document.querySelector("#save");
button.addEventListener("click", (event) => {
event.preventDefault();
console.log("clicked", event.target);
});
Das Event-Objekt enthält nützliche Informationen: target ist das Element, das das Event ausgelöst hat, currentTarget ist das Element, an das der Listener gebunden ist, und preventDefault() verhindert das Standardverhalten des Browsers. Zu den gängigen Events gehören click, input, change, submit, keydown, scroll und DOMContentLoaded.
Event-Delegation
Anstatt jedem Kindelement einen Listener zuzuweisen, hängen Sie einen an ein Elternelement an und untersuchen Sie event.target. Da Events vom Zielelement aus über seine Vorfahren nach oben bubbeln, sieht das Elternelement sie alle.
// delegate.js
const list = document.querySelector("#todos");
list.addEventListener("click", (event) => {
const button = event.target.closest("button.delete");
if (!button) return;
button.closest("li").remove();
});
Dies ist effizienter und sorgt vor allem dafür, dass die Funktionalität auch für Elemente erhalten bleibt, die erst nach dem Hinzufügen des Listeners erstellt wurden. Für dynamische Listen und Tabellen ist Delegation das Standard-Pattern.
Durchlaufen des Baums
Manchmal muss man sich von einem Knoten zu seinen verwandten Knoten bewegen.
// traverse.js
const item = document.querySelector(".item");
item.parentElement; // nearest ancestor element
item.children; // child elements only
item.firstElementChild; // first child element
item.nextElementSibling; // next sibling element
item.closest(".card"); // nearest ancestor matching a selector
Die Element-Varianten überspringen Text- und Kommentar-Knoten, was fast immer das gewünschte Ergebnis ist. closest ist besonders nützlich in Event-Handlern, wie das Beispiel zur Delegation zeigt.
Performance und Best Practices
- Bündeln Sie Lese- und Schreibvorgänge. Das Auslesen von Layout-Eigenschaften wie
offsetHeightzwingt den Browser zur Neuberechnung; das Abwechseln von Lese- und Schreibvorgängen führt zu wiederholten Reflows. - Off-document aufbauen. Verwenden Sie ein
DocumentFragmentoder ein Element außerhalb des sichtbaren Bereichs und fügen Sie es erst am Ende einmalig ein. - Lookups cachen. Speichern Sie Elemente in Variablen, anstatt sie innerhalb einer Schleife erneut abzufragen.
- Klassen gegenüber Inline-Styles bevorzugen. Schalten Sie eine Klasse um und halten Sie die Darstellung im CSS.
- Standardmäßig
textContentverwenden. Nutzen SieinnerHTMLnur bei vertrauenswürdigen, bereinigten Inhalten. - Event-Delegation nutzen für Listen und alle dynamischen Elemente.
- Listener entfernen, die nicht mehr benötigt werden – insbesondere auf langlebigen Seiten –, um Memory Leaks zu vermeiden.
Häufige Fehler
- Ein Element abfragen, bevor es existiert, und sich dann wundern, warum es
nullist. innerHTMLmit Benutzereingaben verwenden und damit eine XSS-Lücke öffnen.- Einen Listener an jede einzelne Zeile hängen, anstatt das Event an die Tabelle zu delegieren.
- Layout-Properties innerhalb derselben Schleife lesen und schreiben.
event.targetundevent.currentTargetverwechseln.- Davon ausgehen, dass
querySelectorAlllive ist, obwohl es sich um einen statischen Snapshot handelt.
Wie geht es weiter?
Das DOM ist der Knotenpunkt für alles, was Sie bisher gelernt haben. Kombinieren Sie es mit der ES6+ Syntax für saubereren Code, versorgen Sie es mit Daten über die Fetch API und machen Sie es durch Error Handling robust. Der schnellste Weg, dies zu verinnerlichen, ist es, etwas Kleines – eine To-do-Liste, eine filterbare Galerie oder eine Live-Suche – komplett in vanilla JavaScript nachzubauen.