Nach drei kurzen Jahren Pause wird es mal wieder Zeit für einen Eintrag im Erfolgsformat „Webentwicklungs-Fragen vom Erklärbär beantwortet“. Es ist nicht so, als hätte es in der Zwischenzeit keine weiteren Fragen von eurer Seite gegeben, aber aus einer Reihe von Gründen konnten diese nicht in Artikel überführt werden. Damit ist jetzt Schluss! Und wenn ihr mein Backlog weiter gefüllt halten möchtet, könnt ihr mir eure Fragen zu Frontend-Themen aller Art wie gewohnt per E-Mail oder Fediverse übersenden.

Was ist so schlimm an getElementsByTagName()?

Du hast in einem Podcast mal erwähnt, dass man getElementsByTagName() nicht benutzen sollte. Warum? Ist es die Spezifikation selbst oder geht es um Performance-Probleme?

Es gibt im Wesentlichen zwei Argumente, die gegen die Benutzung von getElementsByTagName() sprechen: das Vorhandensein einer mächtigeren Alternative und tatsächlich zumindest mögliche Performance-Probleme.

Zum einen gibt nicht wirklich einen Grund, getElementsByTagName() zu verwenden, wenn wir querySelectorAll() als Alternative zur Verfügung haben. Alles, was getElementsByTagName() kann, kann querySelectorAll() auch, plus eine ganze Menge mehr – eben Elemente anhand von mehr als nur ihres HTML-Tags auswählen. Das größere Problem ist aber tatsächlich die Performance und ein etwas unintuitives Verhalten.

getElementsByTagName() liefert eine HTMLCollection mit den selektierten Elementen als Inhalt. Eine HTMLCollection sieht zwar aus, als wäre sie einfach nur ein weiteres der zahllosen (und harmlosen) Listen-Objekte im DOM, ist aber in aller Regel live. DOM-Manipulationen, die nach getElementsByTagName() stattfinden, verändern also das zuvor ermittelte Ergebnis und sorgen auf der Performance-Seite für entsprechenden Mehraufwand. Und intuitives Programmieren sieht auch anders aus!

// Ausgangslage: <div class="a"></div&ht;<div class="b"></div>
const divs = document.getElementsByTagName("div");
console.log(divs.length, divs[0], divs[1]); // > 2, div.a, div.b
document.body.insertAdjacentHTML("afterbegin", '<div class="x"></div>');
console.log(divs.length, divs[0], divs[1]); // > 3, div.x, div.a - WTF!

Wenn selbst die DOM-Spezifikationen HTMLCollection als „a historical artifact we cannot rid the web of“ bezeichnen, ist es vielleicht wirklich an der Zeit, diesen Objekt-Typ und dazugehörige APIs wie getElementsByTagName() nicht mehr zu verwenden.

Neue HTML-Elemente erfinden, ohne sie zu registrieren?

Mir ist aufgefallen, dass ich ein neues HTML-Tag benutzen und auch per CSS gestalten kann, ohne es als Web Component zu registrieren. Alles Wichtige scheint zu funktionieren. Kann es sein, dass so eine spontane Erstellung von Elementen erlaubt ist? Und wenn ja, warum verwenden wir dann überhaupt noch Klassen und IDs? Kann ich nicht einfach statt <div class="mainbox"> gleich <main-box> schreiben?

Dass der Browser angesichts eines unbekannten HTML-Tags nicht mit einer Fehlermeldung abstürzt, sondern versucht, das Beste daraus zu machen, ist ein Fehlerbehandlungsmechanismus. Und das erklärt auch, warum wir unregistrierte HTML-Elemente nicht benutzen sollten: es ist besser, keine Fehler zu machen, als den Browser unsere Fehler ausbügeln zu lassen.

Auf technischer Ebene wir das unbekannte Element vom Browser als HTMLUnknownElement verarbeitet, das für unbekannte Elemente in etwa den Funktionsumfang eines Span-Elements bereitstellt. Klassen, IDs, CSS, Data-Attribute, all das funktioniert in allen gängigen Browsern auf unbekannten Elementen. Sofern der verwendete Tag-Name einen Bindestrich enthält, ist ein solches nicht-angemeldetes unbekanntes Element sogar einigermaßen zukunftssicher, da sich der Tag im geschützten Namensraum für benutzerdefinierte Elemente befindet – es wird also nie ein neues natives Element gleichen Namens eingeführt werden.

Trotzdem ist und bleibt das HTMLUnknownElement ein reiner Fehlerbehandlungsmechanismus, den wir nicht absichtlich einsetzen sollten: nicht nur sehen Webentwickler, die ungültige Elemente verwenden, wie schlampige Handwerker aus, es wäre auch extrem unkompliziert, das Element korrekt zu registrieren:

window.customElements.define(
  "tipp-box",
  class extends HTMLElement {}
);

Das ist so wenig Aufwand, dass nicht wirklich etwas dagegen spricht, es richtig zu machen. Und sollte sich eines Tages herausstellen, dass das Element ein paar Extras wie eingebaute ARIA-Rules benötigt, ist die Klasse, an die diese Extras angebaut werden können, auch bereits vorhanden.

Unregistrierte Elemente fallen in die bei HTML sehr umfangreiche Kategorie „ist nicht erlaubt, funktioniert aber“. Dort befinden sich unter anderem auch Framesets, die auch niemand (mehr) verwendet, obwohl es rein technisch möglich wäre und auch immer möglich bleiben wird.

Bester Weg, zirkuläre Objektstrukturen zu erkennen?

Wie kann ich am besten herauszufinden, ob eine Property eines Objekts eine Referenz auf das eigene Objekt oder Teile des eigenen Objekts besitzt? Ich verlasse mich bisher darauf, dass JSON.stringify() bei zirkulären Objekten einen Fehler wirft, aber vielleicht gibt es ja einen besseren Weg?

Das Wichtigste vorweg: JSON.stringify() wirft zwar tatsächlich Fehler bei zirkulären Objektreferenzen, hat aber auch große blinde Flecken in Form von nicht-JSON-kompatiblen Objekten:

const x = {
  a: new Map(),
};

// Böse zirkuläre Referenz
x.a.set("foo", x);

// Aber kein Fehler: JSON.stringify macht Maps zu {}
JSON.stringify(x);

JSON ist lange vor der Einführung von Maps und JavaScript definiert worden und kann daher mit Maps nichts Sinnvolles anstellen. Stattdessen verwandelt es Maps unter Missachtung ihres Inhalts einfach in {}, wodurch zirkuläre Referenzen unentdeckt bleiben. Andererseits wirft JSON.stringify() aber auch gerne Fehler bei Objekten ohne zirkuläre Referenzen:

// Fehler ohne zirkuläre Referenzen: JSON mag kein BigInt
JSON.stringify({ x: 42n }); // Error!

Wir können JSON.stringify() also durchaus nutzen, um einige Arten von zirkulären Referenzen zu erkennen, aber es ist nicht für alle Use Cases geeignet. Dort, wo JSON.stringify() nicht taugt, könnten wir manuell den Objekt-Tree nach zirkulären Referenzen absuchen, aber auch dann gibt es diverse Edge Cases, für die es nicht immer eine eindeutige beste Lösung geben wird:

  • Sollten Properties, die Symbols oder Non-Enumerable sind, ebenfalls untersucht werden?
  • Objekte könnten privaten State (via privaten Klassenfeldern oder Closures) haben, die zirkuläre Referenzen halten könnten … was für uns aber nicht feststellbar wäre.
  • Objekt-Getter könnten nichtdeterministische Ergebnisse liefern uns sich so der Zirkularitätsfrage entziehen.

Was als der wirklich beste Weg ist, hängt letztlich davon ab, ob und wie wir diese Edge Cases berücksichtigen wollen. Möglicherweise ist JSON.stringify() schon der beste und praktikabelste Kompromiss!

Wie kann ich ein Formular absenden, ohne ein Formular haben zu müssen?

Ich möchte meinem Server vorspielen, jemand hätte ein Formular ausgefüllt und abgeschickt. Wie kann ich das nur mit JavaScript, ohne <form>-Elemente erreichen?

FormData hilft! Einfach ein FormData-Objekt erstellen, mittels append() Daten (Key-Value-Paare) einfügen und dann via fetch() versenden:

const fd = new FormData();
fd.append("foo", "23");
fd.append("bar", "42");
fetch("/", {
  method: "POST",
  body: fd
});

Dass diese Möglichkeit besteht, ist auch tatsächlich nicht schlecht. Für normale User Interfaces ist natürlich ein normales, echtes Formular immer das Mittel der Wahl, denn <form>-Elemente liefern Features wie Barrierefreiheit, Formularvalidierung und JS-Ausfall-Sicherheit zum Quasi-Nulltarif mit. Selbst für abgefahrenste JavaScript-Vorhaben ist es immer besser, ein normales Formular anzulegen und sein Verhalten mit eigener Logik zu ergänzen (z. B. per abgefangenem submit-Event), statt das Rad mit JS von 0 neu zu erfinden.

Aber es gibt auch einige wenige Umstände, unter denen eine reine JS-Lösung das einzig verfügbare Mittel ist, wie etwa Web Worker. Fetch-API und FormData sind auch in Worker Scopes verfügbar, <form>-Elemente hingegen nicht. In diesem Fall führt also kein Weg an einer reinen JavaScript-Lösung vorbei. Gleiches gilt, wenn wir versuchen, ein Form-Associated Custom Element zu bauen, das mehr als einen Value repräsentieren soll (vergleichbar mit einem <select multiple>).

Weitere Fragen?

Habt ihr auch dringende Fragen zu Frontend-Technologien? Nur her damit! Alle Fragen in diesem Post wurden mir per E-Mail oder Fediverse gestellt und ihr könnt das genau so machen! Einfach über einen der genannten Kanäle anschreiben oder gleich das komplette Erklärbären-Paket kommen lassen.