Warum man die Finger von den Prototypen nativer Objekte zu lassen hat

Veröffentlicht am 10. August 2015

In regelmäßigen Abständen schlägt bei mir die Frage auf, ob es nicht in ganz bestimmten Fällen doch in Ordnung wäre, die Prototypen der nativen JavaScript- und DOM-Objekte zu erweitern. Meine Antwort darauf ist immer: in öffentlich zugänglichen Projekten, ist das ein absolutes No-Go. Gegen den Einsatz bei privaten Projekten oder bei Spezialtools wie Testframeworks (z.B. should.js) lassen sich auch Argumente finden, aber darüber kann man durchaus zweierlei Meinung sein.

Wenn es darum geht, Gründe gegen das Erweitern der nativen Built-Ins zu finden, wird oft die Kollisionsgefahr zuerst herausgekramt. Würde man in seinem Code Element.prototype.foo definieren, könnte eine Third-Party-Library jetzt oder in Zukunft auch etwas unter Element.prototype.foo implementieren wollen. Diesem Argument lässt sich entgegensetzen, dass das erstens nie passiert und sich zweitens in 99% aller Fälle problemlos reparieren lassen dürfte – einfach das eigene Element.prototype.foo in Element.prototype.foobar umbenennen und schon funktioniert wieder alles. Aber wenn man vom Start weg nicht den Namen Element.prototype.foo, sondern etwas wie Element.prototype.$_foo wählt, werden derartige Reparaturen höchstwahrscheinlich gar nicht erst nötig werden. Auf Projektebene ist die Kollisionsgefahr mit anderen Libraries praktisch nicht gegeben.

Das größere Problem mit dem Erweitern von nativen Objekten besteht nicht auf der Projekt-, sondern auf der Web-Ebene. Browser und Spezifikationen müssen bei der Entwicklung neuer Features immer darauf achten, dass die Neuheiten kompatibel zum gesamten existierenden, öffentlichen HTML/JavaScript/CSS-Code sind. Sie haben es also mit der denkbar größten Legacy-Codebase der Welt zu tun, nämlich dem kompletten WWW. Das macht die Entwicklung von Neuheiten recht knifflig, nicht zuletzt weil einige Entwickler nicht die Finger von den Prototypen nativer Elemente lassen konnten. Das beste Beispiel dafür ist die in ECMAScript 6 neu eingeführte Funktion Array.prototype.includes(), die prüft ob ein Array ein gegebenes Element enthält. Ursprünglich sollte das neue Feature den Namen Array.prototype.contains() tragen, doch es stellte sich heraus, dass mehrere Webseiten bereits länger eine genau so benannte, aber subtil anders implementierte Funktion gleichen Namens verwenden. Diese Funktion stammt aus der JavaScript-Library Mootools, die schon vor Jahren den Zenit ihrer Popularität überschritten hatte. Aber da es immer noch Webseiten gibt, die Mootools in der einen oder anderen Version verwenden, war der Name Array.prototype.contains() für ECMAScript 6 nicht zu gebrauchen. Es erfolgte die Umbenennung in includes() und in Folge musste natürlich auch die entsprechende String-Methode für ES6 umbenannt werden.

Das Beispiel zeigt, was passieren kann, wenn man als normaler Webentwickler die Prototypen von nativen Built-Ins erweitert: man fuhrwerkt im Legacy-Code jeder Browserengine und jeder Spezifikation herum, die es jemals gab und jemals geben wird! Und da man nicht weiß, in welche Richtung die Entwicklung der Webstandards in 3 Jahren gehen wird, sollte man tunlichst die Finger von den entsprechenden Objekten lassen. Vielleicht wird es nie ein Problem sein, sein eigenes Element.prototype.$_foo zu definieren. Vielleicht wird man genau damit aber auch ein Feature von DOM 6.0 oder ECMAScript 12 blockieren.

Zusammengefasst ist es in Ordnung, die nativen Prototypen zu erweitern, wenn entweder

  1. das Projekt privat ist (z.B. Intranet-App, Testing-Tool) und die entsprechenden Codezeilen auch niemals das WWW betreten werden, und sei es durch Copy & Paste in ein neues Projekt
  2. oder man sicher weiß, dass der entsprechende Code niemals mit irgendeinem zukünftigen Standard kollidieren wird oder dass zu dem Zeitpunkt, an dem die Kollision geschehen wird, garantiert vom fraglichen Code keine einzige Zeile mehr irgendwo im WWW aufzufinden ist.

Ich weiß nicht, wie man diese Bedingungen erfüllen will, ohne Hellseher zu sein. Daher lasse ich die Finger von den Prototypen der nativen Built-Ins und empfehle das auch jedem, der mich fragt.

Fragen zu HTML5 und Co beantwortet 21 - Semantische Elemente, Source Maps, das a-Element, Whitespace

Veröffentlicht am 13. Juli 2015

Die nebenstehende Fragen-Artikel-Box wird so langsam zu lang. Für das nächste Redesign ist eine Einklapp-Funktionalität fest eingeplant, aber bis dahin müssen wir das Layout noch so ertragen wie es ist. Falls ihr euren Teil zur Verlängerung der Box betragen wollt, schickt auch ihr mir einfach eure Fragen zu HTML5 und Co.

HTML5-Elemente nicht benutzen – ein Fehler?

Ich habe noch nie wirklich HTML5-Elemente oder -Attribute verwendet. Ich benutzte eigentlich nur <div> mit data-*-Attributen und viel JavaScript. Mache ich etwas falsch?

Im Prinzip ist das durchaus ein Fehler. Wer keine HTML5-Elemente verwendet, verwendet andere Elemente, und über diese sagt die Spezifikation: Authors must not use elements [...] for purposes other than their intended semantic purpose. Und über das <div>-Element im Speziellen heißt es: Authors are strongly encouraged to view the div element as an element of last resort, for when no other element is suitable.

Ob man sich mit der Verletzung dieser Regeln wirklich einen wahrnehmbaren Nachteil einhandelt, steht freilich auf einem anderen Blatt. Jenseits aller semantischer Theorie besteht der Hauptunterschied zwischen den neuen semantischen HTML5-Elementen und dem guten alten <div> in den bei den neuen Elementen eingebauten Barrierefreiheits-Features. Ob man auf diese nun gesteigerten Wert legt oder nicht – es spricht nichts dagegen, diese einfach mitzunehmen und statt die <div>-Suppen mit ein paar <nav>- und <section>-Elementen zu dekorieren. Es kostet schließlich nichts.

Source Maps und mehrere Kompilier-Schritte

Source Maps sind schön und gut, aber was tun bei mehreren Kompilier-Schritten? Wenn ich z.B. mit Babel transpilierten Code erst durch Browserify und dann UglifyJS jagen möchte, bekomme ich doch keine Source Map mehr auf den Ausgangs-Quellcode mehr, oder?

Aber klar geht das! Die meisten der genannten Tools unterstützen Input-Source-Maps, UglifyJS z.B. über den Parameter --in-source-map. Diese Source Map wird dann als Ausgangslage für alle folgenden Transformationen genommen. Bei Inline-Maps (d.h. wenn die Source Map als Data-URL direkt in die JavaScript-Datei geschrieben wird) machen das die meisten Tools sogar automatisch. Und falls das mal nicht der Fall ist, kann man mittels Exorcist die Inline-Maps aus der Datei extrahieren und diese dann wiederum als Input für andere Tools nutzen.

Das <a>-Element in HTML5

Ist <ul><a href="#test"><li>Test</li></a></ul> gültiges HTML5? Es sieht nicht so aus, aber ich dachte bis gerade eben, man könnte in HTML5 um alles ein <a>-Element legen …

Nicht ganz: in HTML5 kann ein <a>-Element fast alles enthalten. Vor HTML5 waren HTML-Elemente in die zwei Kategorien Block-Elemente und Inline-Elemente eingeteilt und es galt die Regel: Inline-Elemente (wie <a>) dürfen keine Block-Elemente enthalten.

In HTML5 ist die Kategorisierung viel feinteiliger ausgefallen. Es gibt insgesamt sieben wichtige Sorten von Inhaltskategorien, wobei ein Element in mehrere Kategorien fallen kann. In welche Kategorie ein Element fällt, welche Elemente es enthalten kann und in welchen anderen Elementen es auftauchen darf, ist für das betroffene jeweils individuell Element definiert. Das <a>-Element darf beispielsweise überall auftauchen, wo sogenannter phrasing content erlaubt ist und es kann alles enthalten, was sein Elternelement enthalten darf – solange es sich nicht um interactive content, d.h. Inputs, andere Links etc. handelt.

Das <ul>-Element fällt in die Kategorie flow content. Es darf also auch in <a>-Elementen vorkommen, vorausgesetzt das Elternelements des <a>-Elements erlaubt flow content als Inhalt. Das war früher noch anders, denn da galten einfach <a> als Inline- und <ul> als Block-Element – die Kombination <a><ul></ul></a> war also nicht erlaubt. In HTML5 ist das hingegen kein Problem. Allerdings dürfen <ul> und auch <ol> weiterhin nur <li>-Elemente und Scripts als ihre Kindelemente haben.

Whitespace in HTML5

Definiert HTML5 irgendwo, wie der Browser mit Whitespace am Anfang oder Ende eines Elements umgehen soll? Sollten beispielsweise folgende Elemente immer gleich dargestellt werden (wobei „text“ hier für jedes Element mit Text darin stehen kann)?

text
[leerzeichen]text
text[leerzeichen]
[leerzeichen]text[leerzeichen]

Die Frage definiert HTML5 [kleines HTML-Syntax-Detail] … ist eigentlich immer mit Ja zu beantworten. HTML5 definiert den HTML-Parser bis ins kleinste Detail und dazu gehört auch die Verarbeitung von Whitespace.

Whitespace vor/nach/zwischen Elementen ist erlaubt (Inter-element whitespace) und führt zur Erzeugung von Textknoten mit nichts als eben dem Whitespace darin. Wenn wir im o.g. Beispiel mal <span>-Elemente als Beispiel-Non-Whitespace-Elemente hernehmen, sollte immer dieser DOM-Baum produziert werden:

  1. <span>A​</span>
  2. Textknoten "↵ " (Zeilenumbruch + führendes Leerzeichen)
  3. <span>B​</span>​
  4. Textknoten "↵" (Zeilenumbruch)
  5. <span>C​</span>​
  6. Textknoten " ↵ " (hinteres Leerzeichen + Zeilenumbruch + führendes Leerzeichen)
  7. <span>C​</span>​
  8. Textknoten " " (hinteres Leerzeichen)

Voraussetzung ist natürlich ein Browser mit ordentlichem HTML5-Parser an Bord.

Das Rendering dieser Whitespace-Textknoten ist durch die CSS-Eigenschaft white-space und nicht HTML5 definiert. Dort ist als Standardverhalten festgelegt, dass benachbarte Leerzeichen zusammengefasst bzw. alle bis auf eins auf eine Breite von 0 gequetscht werden. Mit anderen Werte für die white-space-Eigenschaft kann man das aber bequem den eigenen Wünschen anpassen.

Weitere Fragen?

All diese Fragen wurden mir per E-Mail oder Twitter gestellt und auch eure Fragen zu HTML(5), CSS(3), JavaScript und anderen Webtechnologien beantworte ich gerne! Einfach über einen der genannten Kanäle anschreiben oder gleich das komplette Erklärbären-Paket kommen lassen.

Erklärbär-Termine für Juli, August und September

Veröffentlicht am 15. Juni 2015

In den nächsten Monaten bin ich vor allem mit Inhouse-Schulungen beschäftigt, aber es gibt auch wieder einen öffentlichen Erklärbär-Termin:

  • 3. - 5. August in München: Moderne Frontendentwicklung (HTML5, CSS3, JavaScript). HTML5 … und dann? Die Schulung „Moderne Frontendentwicklung“ ist der Nachfolger meiner klassischen HTML5-Schulung und hebt erfahrene Webentwickler auf das nächste Level. Der Kurs behandelt die seit HTML5 neu hinzugekommenen Webstandards (z.B. Web Components), bespricht Tools und Best Practices, gibt Tipps für den Einsatz neuer Features in heutigen Browsern und bietet auch einen Ausblick in die weitere Zukunft der Web-Plattform.

Termine unpassend, Orte alle zu weit weg und Programme nicht genehm? Ich komme auch gerne mit einem maßgeschneiderten Workshop vorbei – mich kann man ganz einfach mieten!

Fragen zu HTML5 und Co beantwortet 20 - Graustufen, SQL-Datenbank, Tail Recursion, Arrow Functions in Klassen

Veröffentlicht am 8. Juni 2015

Der beliebte Fragen-beantwort-Service von Erklärbär Enterprises meldet sich zurück, heute mit CSS-Filtern, HTML5-Datenbanken und zwei Fragen zu ECMAScript 6. Wenn auch euch Fragen zu Frontend-Themen wie HTML5, CSS3, JavaScript, Tools und ähnlichem habt, dann schickt sie mir: eine E-Mail oder ein Tweet genügen! Die Fragen werden beantwortet, gesammelt und wenn vier Antworten zusammen gekommen sind, folgt die Veröffentlichung.

Cross-Browser-Graustufen für Bilder?

Gibt es eine zuverlässige Möglichkeit (auch für den IE) Bilder via CSS oder JavaScript grau einzufärben?

In jedem modernen Browser mit Ausnahme der Internet-Explorer-Familie ist ein Graustufen-Effekt mit CSS-Filtern (Can I Use) ein Kinderspiel. Mit Filtern kann man jeden denkbaren SVG-Filter auf jedes beliebige Element in einer Webseite anwenden und für die häufigsten Anwendungsfälle wie eben Graustufen gibt es eine praktische Kurzschreibweise, bei der man sich den SVG-Part ersparen kann (Demo auf Codepen):

img {
  -webkit-filter: grayscale(100%);
  filter: grayscale(100%);
}

Dummerweise machen die neueren Internet Explorer nicht mit. Die IE 6-9 kann man mit einer vergleichsweise simplen Code-Transformation, wie sie in Schepps CSS-Filters-Polyfill vollführt wird, bedienen, denn diese unterstützen eine proprietäre Syntax mit ähnlichen Fähigkeiten wie filter. Allerdings wurde diese proprietäre Syntax im Zuge des großen Aufräumens aus dem IE 10 entfernt, so dass es dort keine Möglichkeit gibt, Graustufen-Filter mit CSS allein umzusetzen.

Einzig denkbarer Ausweg: das <canvas>-Element. Dieses funktioniert auch im IE9 und könnte genutzt werden, um normale <img>-Elemente zu klonen und sie im Zuge dessen in Graustufen zu konvertieren. Eine kleine Demo gibt es hier. Die Haken an dieser Methode sind die JavaScript-Abhängigkeit und natürlich, dass sich hiermit nur Bild-, Canvas- und Video-Elemente in Graustufen darstellen lassen – CSS-Filter funktionieren auf allen Elementen.

Was ist Tail Recursion? Und was hat das mit ECMAScript 6 zu tun?

Was ist „Tail Recursion“ und warum liest man im ES6-Kontext ständig davon?

Gibt man einer heutigen JavaScript-Engine eine Aufgabe, die viel Rekursion beinhaltet, bekommt man häufig einen Fehler der Marke maximum call stack size exceeded oder too much recursion vorgesetzt. Das liegt einfach darin begründet, dass der Call Stack, der die Liste der sich einander aufrufenden Funktionen beinhaltet, nicht beliebig groß werden kann. Beispiel:

function f(x){
  x++;
  if(x === 5000000){
    return 'Fertig nach ' + x + ' Aufrufen von f()';
  }
  return f(x);
}

console.log(f(0));
// Error: Maximum call stack size exceeded

Hier ruft f() immer wieder sich selbst auf und der Browser muss sich für jeden Aufruf das jeweils übergebene x und vieles mehr merken. Der Call Stack wächst und wächst, denn als fertig gilt ein Aufruf von f() erst, wenn alle enthaltenen f()-Aufrufe fertig sind. So wird der Call Stack immer größer, bis er irgendwann ein ein Limit stößt und es laut knallt.

Allerdings muss es in diesem Beispielcode eigentlich nicht zwangsläufig knallen, denn die Rekursion findet in f() als allerletzte Aktion statt – es handelt sich um einen soganannten Tail Call. Solche Tail Calls können durch die den Interpreter oder Compiler einer Programmiersprache so optimiert werden, dass sie keinen Extra-Platz auf dem Call Stack benötigen und viel tiefere Rekursion möglich wird. Optimiert könnte das obrige Programm genau so gut wie folgt aussehen:

function f(_x){
  var _again = true;
  _function: while(_again){
    _again = false;
    var x = _x;
    x++;
    if(x === 5000000){
      return 'Fertig nach ' + x + ' Aufrufen von f()';
    }
    _x = x;
    _again = true;
    continue _function;
  }
}

console.log(f(0));

Die rekursive Konstruktion wird zu einer Schleife umgebogen, belastet den Call Stack nicht mehr und kann daher wesentlich länger laufen. Voraussetzung für derartige Optimierungen ist lediglich, dass die Rekursion wirklich die aller-allerletzte Aktion in der fraglichen Funktion ist.

Mit ECMAScript 6 hat das Ganze nur insofern zu tun, als dass ab dieser Version die Optimierung von Tail Calls für alle JavaScript-Engines verbindlich vorgeschrieben sein wird. Bis das in allen Browsern der Fall ist, klappt es bei so einfacheren Fällen wie dem obrigen Beispiel auch mit Babel.

SQL-Datenbank im Browser?

Ich möchte zur Sicherung der Offline-Funktionalität meine Webapp mit einem MySQL-Backend synchronisieren. Hierbei müssten auch Abfragen mit JOINS möglich sein, also über mehrere Tabellen. Gibt es eine passende Browsertechnologie hierfür?

Eine SQL-Datenbank im Browser gibt es nicht und wird es nie geben. Die Spezifaktion für die Web SQL Database ist seit 5 Jahren tot und die Browserunterstützung wird nicht mehr besser werden – eher schlechter. Die moderne Alternativ-Technologie Indexed DB (Specs, Can I use) hingegen ist gesund und munter, ist aber auch eine NoSQL-DB, bei der es prinzipbedingt Dinge wie Joins nicht gibt.

Die Lösung besteht wie so oft darin, JavaScript auf das Problem zu werfen. Libraries wie Lovefield implementieren auf Basis der Indexed DB eine Query-Sprache, die zwar nicht ganz SQL ist, aber ungefähr das gleiche auszudrücken vermag.

ES6: Arrow Functions in Klassen?

Wie kann ich in ES6 Arrow Functions in Klassen benutzen? Ich habe eine Klassen-Methode getBar(), die immer das this des von der Klasse erzeugten Objekts benutzen soll. Arrow Functions scheinen in Klassen syntaktisch nicht zugelassen zu sein, aber ich möchte gerne Klassen-Methoden als Callbacks verwenden können ohne ständig fooInstance.getBar.bind(fooInstance) schreiben zu müssen.

Zur Verdeutlichung: das Ziel ist, dass das Folgende funktioniert:

class Foo {
  constructor(){
    this.bar = 'test';
  }
  getBar(){
    console.log(this.bar);
  }
}

var f = new Foo();
setTimeout(f.getBar, 1000);

Ohne f.getBar.bind(f) oder eine ähnliche Konstruktion wird das allerdings nicht funktionieren – Arrow Functions können hier nicht helfen. Das Problem ist, dass die Klassensyntax von ES6 nur syntaktischer Zucker für die althergebrachten JS-Prototypen ist. Der obrige ES6-Code entspricht (grob) dem folgenden ES5:

function Foo(){
  this.bar = 'test';
}
Foo.prototype.getBar = function(){
  console.log(this.bar);
}

var f = new Foo();
setTimeout(f.getBar, 1000);

Hier sieht man sofort, dass getBar() nicht auf ein this festgenagelt werden kann, da das Ziel-this zum Zeitpunkt der Definition getBar() nicht bekannt ist bzw. getBar() als Prototypen-Methode mit den unterschiedlichen this der unterschiedlichen Foo-Instanzen arbeiten muss. Es handelt sich hier also um keine syntaktische, sondern eine ganz grundsätzliche Hürde.

Was also tun? Am besten tatsächlich brav jedes Mal f.getBar.bind(f) verwenden, denn das ist am einfachsten und am deutlichsten. Alternativ könnte man natürlich auch jeder Instanz eine eigene getBar()-Methode geben, die sich das this per Closure merkt:

class Foo {
  constructor(){
    this.bar = 'test';
    this.getBar = function(){
      console.log(this.bar);
    }.bind(this)
  }
}

var f = new Foo();
setTimeout(f.getBar, 1000);

Insgesamt ist bind() aber schon die sauberere Lösung.

Weitere Fragen?

Auch eure Fragen zu HTML(5), CSS(3), JavaScript und anderen Webtechnologien beantworte ich gerne! Einfach eine E-Mail schreiben oder Twitter bemühen und ein bisschen Geduld mit der Antwort haben. Ansonsten kann man mich natürlich auch als Erklärbär zu sich kommen lassen.