JavaScript-Funktionen mit definierter length-Eigenschaft erzeugen: drei Ansätze

Ich habe mich in den letzten Wochen viel mit JavaScript-Funktionen beschäftigt, die andere JavaScript-Funktionen umbauen. Ein gutes Beispiel für solche Funktionen ist die_.flip()-Funktion von Lodash, die die Parameter-Reihenfolge einer Funktion umdreht:

function oldFn (a, b, c) {
  return a + b + c;
}

let newFn = _.flip(oldFn);

console.log(newFn("a", "b", "c")); // > "cba"

Funktionsumbau-Funktionen finde ich besonders dann nützlich, wenn ich eine Kompatibilitätsschicht bauen möchte, weil z.B. ein bestimmtes Modul plötzlich wegen eines Updates ganz andere Funktionsnamen und Parameter-Reihenfolgen hat. Statt den kompletten eigenen Code auf die neue API anzupassen kann man oft auch einfach die alte API rekonstruieren diese Kompatibilitätsschicht zwischen den eigenen, nicht weiter zu ändernden Code und das problematische Modul klemmen. Häufig kommt man dabei sehr weit, indem man einfach die neuen Funktionen des problematischen Moduls nimmt und sie mit Funktionen wie eben _.flip() oder _.rearg() wieder in die alte Form bringt.

Es gibt da nur ein kleines Problem:

function oldFn (a, b, c) {
  return a + b + c;
}

let newFn = _.flip(oldFn);

console.log(oldFn.length); // > 3
console.log(newFn.length); // > 0

So gut wie immer liefern Funktionsumbau-Funktionen Funktionen, die eine length von 0 haben. Ein „Funktionsumbau“ bedeutet immer, dass um eine gegebene Eingangs-Funktion ein Wrapper konstruiert wird, der zusätzliches Verhalten implementiert und am Ende des Tages dann doch wieder die Eingangs-Funktion aufruft. So könnte man flip() zum Beispiel wie folgt bauen:

function flip (inputFn) {
  return function (...args) {
    return inputFn.apply(this, args.reverse());
  };
}

Die Funktion, die flip() zurückgeben würde, hätte natürlich eine length von 0, da sie variadisch ist – sie hat ja auch nichts weiter zu tun, als die an sie übergebenen Parameter in umgekehrter Reihenfolge an inputFn() durchzureichen. Das Problem dabei ist, dass Funktionen ohne definierte length manchmal schlecht weiter umbauen lassen, denn viele Umbau-Operationen müssen wissen, wie viele Parameter eine Funktion haben möchte. Currying ist ein solches Beispiel:

function oldFn (a, b, c) {
  return a + b + c;
}

let flippedFn = _.flip(oldFn);

let curriedFlippedFn = _.curry(flippedFn);

console.log(curriedFlippedFn("a")); // > "aundefinedundefined"

Currying ist das Zerlegen einer Funktion mit N Parametern in N Funktionen mit einem Parameter … was natürlich nur klappt, wenn N (d.h. die length) bekannt ist! In Lodash könnten wir den gewünschten N-Wert als zweiten Parameter _.curry() hineinstecken, aber das ist doch eigentlich absurd! Kann man kein _.flip() konstruieren, das Funktionen mit der korrekten length liefert? Es stellt sich raus: man kann! Es gibt sogar mehrere Lösungen. Und die haben alle ihre ganz eigenen Vor- und Nachteile.

Das Ziel

Ich möchte eine Funktion ary(fn, n) konstruieren, die eine Funktion mit length = n zurückgibt. Diese wiederum liefert das Ergebnis des Aufrufs von fn mit nur den den ersten n an sie übergebenen Parametern.

Lodash hat eine solche Funktion, die jedoch nur den zweiten Teil des Ziels erreicht. Sie reicht nur die ersten n Parameter weiter, hat aber immer noch die falsche length. Für viele Fälle ist das auch völlig ausreichend. Der geneigte JavaScriptler hat vielleicht schon mal dieses Problem angetroffen:

[ "0", "1", "2" ].map(parseInt);
// Ergibt [ 0, NaN, NaN ]

Das Ergebnis kommt zustande, indem die map()-Methode die Funktion parseInt() mit mehr als einem Parameter aufruft:

// Was eigentlich passiert
[ "0", "1", "2" ].map(function (str, idx, array) {
  return parseInt(str, idx, array);
});
// > [ 0, NaN, NaN ]

parseInt() erhält neben dem zu parsenden String noch den Index des Strings in dem Array sowie das Array selbst. Der Index wird dabei als Basis für das Zahlensystem verwendet und der dritte Parameter einfach ignoriert. Mit der Lodash-Funktion _.ary() können wir parseInt() zu einer Funktion umbauen, die nur einen Parameter annimmt und für alle übrigen Werte auf ihre Defaults zurückgreift:

// Baut einen Wrapper um parseInt(), der nur einen Parameter weitergibt
var unaryParseInt = _.ary(parseInt, 1);

console.log([ "0", "1", "2" ].map(unaryParseInt));
// > [0, 1, 2]

// Problem: length === 0
console.log(unaryParseInt.length)
// > 0

Dann probieren wir doch mal, unser eigenes ary(fn, n) mit richtiger length zu bauen. Ich habe hierfür drei Methoden gefunden: die Hirn-Aus-Methode, die Holzhammer-Methode und die universelle Methode.

Die Hirn-Aus-Methode

Die wohl einfachste denkbare Lösung sieht wie folgt aus:

function ary (fn, n) {
  if (n === 0) return function () { return fn.call(this); };
  if (n === 1) return function (a) { return fn.call(this, a); };
  if (n === 2) return function (a, b) { return fn.call(this, a, b); };
  if (n === 3) return function (a, b, c) { return fn.call(this, a, b, c); };
  if (n === 4) return function (a, b, c, d) { return fn.call(this, a, b, c, d); };
  if (n === 5) return function (a, b, c, d, e) { return fn.call(this, a, b, c, d, e); };
  throw new Error("NOPE!")
}

Die Vorteile dieses Ansatzes sind zahlreich:

  1. Funktioniert in jedem noch so alten Browser
  2. Simpler Code, den jeder sofort versteht
  3. Bei Bedarf sehr einfach zu debuggen oder zu erweitern
  4. Garantiert schnell

Eine meiner Programmier-Leitlinien lautet „clever ist das Gegenteil von intelligent“ – lieber ein Problem erst mal auf die billige Tour lösen, bevor man sich in Overengineering verliert. Daher finde ich diesen sehr simplen Ansatz erst mal ganz sympathisch. Dass sich mit dieser Variante maximal fünfstellige Funktionen erzeugen lassen, würde ich nicht als Problem betrachten. Erstens sollte man nie mehr als fünf Parameter brauchen und zweitens ließe sich die Funktion bei Bedarf problemlos entsprechend erweitern. Was will man mehr?

Die Holzhammer-Methode

Wenn die Hirn-Aus-Methode mal zu billig daherkommt, gibt es noch eine weitere Möglichkeit … moderne JavaScript-Engines vorausgesetzt. Seit ES6 ist die length-Eigenschaft von Funktionen veränderbar. Während sie in ES5 noch non-configurable war, ist diese Limitierung in ES6 entfallen. Also lässt sich das Problem ganz einfach mit Object.defineProperty() lösen:

function ary (fn, n) {
  const wrapper = function (...args) {
    return fn.apply(this, args.slice(0, n));
  };
  Object.defineProperty(wrapper, "length", {
    value: n,
    configurable: true
  });
  return wrapper;
}

Sofern die Zielplattform dieses subtile ES6-Feature unterstützt, ist damit unser Ziel erreicht. Bei diesem Ansatz könnte man die Gelegenheit auch nutzen, der zurückgegebenen Funktion einen Namen zu verpassen, indem man die name-Property (ebenfalls in ES6 configurable) überschreibt. Das sorgt im Fehlerfall für schönere Stack Traces, denn im Moment wäre unsere Wrapper-Funktionen schließlich noch anonym und würde im Stack Trace bestenfalls als „wrapper“ benannt. Aber das muss ja nicht sein:

function ary (fn, n) {
  const wrapper = function (...args) {
    return fn.apply(this, args.slice(0, n));
  };
  Object.defineProperties(wrapper, {
    length: {
      value: n,
      configurable: true
    },
    name: {
      value: `${n}ary${fn.name}`,
      configurable: true
    }
  });
  return wrapper;
}

function foo (callback) {
  callback();
}

var unaryFoo = ary(foo, 1);

unaryFoo(function cb () {
  throw new Error();
});

/* Uncaught Error
    at cb (test.js:19)
    at foo (test.js:14)
    at 1aryfoo (test.js:3)/*

Der einzige Haken an der Holzhammer-Methode ist, dass sie moderne JS-Engines voraussetzt und Transpiler oder Polyfills nicht helfen können. Für die älteren Browser brauchen wir also noch eine andere Lösung.

Die Function-Constructor-Doppelwrapper-Methode

Mit dem Function-Constructor lässt sich problemlos eine n-stellige Funktion erzeugen:

new Function ([arg1[, arg2[, ...argN]],] functionBody)

Wenn wir über den unappetitlichen Aspekt hinwegsehen, dass wir den Funktionscode als String bereitstellen müssen, gibt es aber noch ein weiteres Problem: haben wir einen n-stelligen Wrapper mit dem Function-Constructor erzeugt, bekommen wir die zu wrappende Funktion nicht mehr ohne weiteres dort hinein! Dem Function-Constructor entsprungene Funktionen sind keine normalen Closures, sondern werden immer im globalen Scope erzeugt. Die innerhalb von ary(fn, n) erstellte Funktion kann weder n noch fn sehen, wobei zumindest letzteres nötig wäre – irgendwann muss der Wrapper schließlich die Original-Funktion aufrufen! Also machen wir das folgende:

  1. Eine einstellige Funktion wird dem Function-Constructor erzeugt. Der eine Parameter ist die zu wrappende Funktion.
  2. Die mit dem Function-Constructor erzeugte einstellige Funktion lassen wir eine n-stellige Funktion zurückgeben, die die zu wrappende Funktion aufruft (das ist der Parameter der erzeugenden einstelligen Funktion)
  3. Das Ergebnis des Aufrufs der mit dem Function-Constructor erzeugten Funktion mit der zu wrappenden Funktion wird aus ary(fn, n) zurückgeben

Das liest sich in Prosa sehr viel komplizierter als, es am Ende im Code ist. Eigentlich ist das umständlichste, die Parameter-Liste für die n-stellige Funktion zu erzeugen (ein String "x1, x2, xn"):

function ary (fn, n) {
  const argsList = Array(n).fill("x").map((x, i) => x + i).join(",");
  const name = `_${n}ary${fn.name}`;
  const createWrapper = new Function("fn", `return function ${name} (${ argsList }) {
    return fn.call(this, ${ argsList });
  }`);
  return createWrapper(fn);
}

Dieser Ansatz funktioniert mit im Prinzip jedem Browser. Für ältere JavaScript-Umgebungen müssten wir auf die Template-Literals verzichten und die Erzeugung der argsList angepasst werden, aber das wäre kein großes Problem. Der Name der Wrapper-Funktion erhält hier einen Unterstrich als Präfix, da new Function() im Prinzip ein eval() darstellt und sich der JS-Parser an einem Funktionsnamen, der mit einer Zahl beginnt, stören würde. Vermutlich ist damit zu rechnen, dass diese Lösung wegen des Einsatzes von new Function() von allen drei Möglichkeiten nicht die beste Performance bietet.

Zusammenfassung

Im direkten Vergleich gibt es keinen klaren Sieger unter den drei Methoden:

  • Hirn-aus-Methode: simpel, schnell, auch in alten Browsern, Maximal-Length limitiert
  • Holzhammer-Methode: universell, Funktionsnamen möglich, nur in modernen Browsern
  • Function-Constructor-Doppelwrapper-Methode: universell, auch in alten Browsern, Funktionsnamen möglich, komplexer Code, eval (via Function-Constructor)

Alles in Allem bin ich mir auch nicht sicher, welche Methode mir am besten gefällt. Die Hirn-aus-Methode ist mit Sicherheit der in jeder Hinsicht am limitierteste Kandidat, aber spielen diese Limitierungen wirklich eine Rolle? Bis runter zu welchem Browser-Fossil funktioniert die Holzhammer-Methode? Wie langsam sind die von der Function-Constructor-Doppelwrapper-Methode erzeugten Funktionen wirklich und würde das in der Praxis eine spürbare Auswirkung haben? Das Thema verdient weitere Forschung.

Man kann nicht kein JavaScript-Framework verwenden

Ich war vor ein paar Wochen bei einer Firma zu Besuch, die für eines ihrer Produkte ein neues Webfrontend plant. Der JavaScript-Framework-Gretchenfrage stehe ich persönlich recht teilnahmslos gegenüber. Mein Eindruck ist, dass keins der populären Frameworks wesentlich schlimmer ist als die anderen populären Kandidaten. Entsprechend nutzlos ist es meistens, mich zu fragen, ob denn jetzt Angular, React, Aurelia oder Vue so sehr viel besser wäre. Aber im Fall dieser Firma hatte ich zu dieser Frage eine Meinung.

In besagter Firma gab es bei diesem Framework-Streit eine Fraktion, die für eine sogenannte „No-Framework“-Variante agitierte. Die Argumentation dieser Fraktion war, dass man sich sehr viel Lernaufwand und Stress sparen könnte, indem man einfach gar kein Framework verwendet, sondern sich ganz auf jQuery und native Features besinnt. Besonders dem wild vor sich hinmutierenden JS-Ökosystem stand man etwas skeptisch gegenüber und fürchtete sich vor bald schon nicht mehr gepflegten Frameworks und Dependencies. Dem Problem wollte man sich entziehen, indem man einfach kein Framework verwendet, dem die Weiterentwicklung versagt bleiben könnte.

Das Problem von „No-Framework“ ist, dass es diesen Ansatz für moderne Frontend-JS-Webapps gar nicht gibt und es etwas unaufrichtig ist, ihn als Null-Kosten-Alternative (Kosten im Sinne von Risiko, Einarbeitungszeit etc.) den populären JS-Frameworks gegenüberzustellen.

Was ist ein Framework? Ein Framework besteht aus Konventionen und Software, die bei der Arbeit im Rahmen dieser Konventionen assistiert. Verwendet man ein Framework, nimmt einem dieses im Idealfall die Arbeit an diesen zwei Punkten ab – Konventionen werden vorgegeben und passende Software wird bereitgestellt. Was passiert, wenn man kein Framework verwendet? Dann werden sich trotzdem Konventionen und Software herausbilden, die dann die Rolle des Frameworks übernehmen. Wer Programmcode schreibt, setzt naturgemäß Konventionen um und schafft Struktur und erzeugt über kurz oder lang sein eigenes Frameworks. Vielleicht ist dieses Framework ein dokumentiertes, getestetes Software-Modul, vielleicht ist aber nur ein Satz an über das Projekt verteilten, selbstgestrikten Funktionen mit Glue Code. Vielleicht sind die Konventionen in einem Styleguide festgehalten, vielleicht sind sie aber auch nur geheimes Templerwissen der wenigen Eingeweihten. Aber vorhanden sind Konventionen und Software in jedem Fall.

So gesehen kann man also nicht kein Framework verwenden. Man kann sich höchstens selbst eins schreiben. Macht man das gut, schreibt man ein explizites, getestetes, dokumentiertes Stück Software. Macht man es weniger gut, hat man sich am Ende trotzdem ein eigenes Framework geschaffen – nur eben ein implizites, das zwischen den eigentlichen Codezeilen lebt. Wer „No-Framework“ sagt, meint damit tatsächlich „ich schreibe mir selbst ein Framework“. Ich will auch gar nicht ausschließen, dass das unter entsprechenden Voraussetzungen eine gute Wahl sein kann. Nur wenn man die Nachteile der populären JS-Frameworks auflistet muss man diese gegen die Nachteile einer Eigenentwicklung (Zeitaufwand, Risiko usw.) aufwiegen und nicht einfach behaupten, man könnte auf Konventionen und Struktur verzichten oder die Entwicklung von Konventionen und Struktur wäre ein Selbstläufer.

Ein ironiefreier Anwendungsfall für new Number() in JavaScript

Obwohl ich nicht erst seit gestern JavaScript schreibe, gibt es noch ein paar „Features“ in der Sprache, die ich nie in ernst gemeintem Code eingesetzt habe. Dabei gehöre ich nicht mal zur Spanische-Inquisition-Fraktion, die jeden, der mal new Function() geschrieben hat, in den Kerker werfen möchte – wenn etwas funktioniert und in einem speziellen Fall keine Nachteile hat, dann verwende ich es! Nur new Number() hatte ich noch nie eingesetzt … bis vor kurzem.

Wir erinnern uns: JavaScript kennt Wrapper-Objekt für die Datentypen String, Number und Boolean. Diese werden mit new String(x), new Number(x) und new Boolean(x) erzeugt. Durch diese Funktionsaufrufe wird der Parameter x in den jeweiligen Datentyp konvertiert (also zu String, Number und Boolean, je nachdem) und in ein Wrapper-Objekt eingepackt. Ohne new machen die Funktionen nur die Konvertierung und erzeugen keinen Wrapper. Das ist eigentlich immer die sinnvollere Variante, denn die Wrapper-Objekte sind zu nichts gut:

  • Die Wrapper-Objekte bieten keine zusätzliche wünschenswerte Funktionalität. Methoden wie z.B. toExponential() bei Number können auch auf den primitiven Werten verwendet werden – diese verhalten sich diesbezüglich immer wie ein Objekt, auch wenn sie selbst keins sind (denn sie wissen, was sie für ein Objekt sie wären, wenn sie eins wären).
  • Der typeof-Operator identifiziert jedes Wrapper-Objekt (egal ob für String, Number oder Boolean) als object, was korrekt, aber nicht hilfreich ist.
  • Alle Wrapper-Objekte sind ausnahmelos truthy, keins ist jemals falsy. Ja, new Boolean(false) ist truthy, denn alle Objekte sind truthy, auch wenn sie ein false wrappen oder einfach nur leer sind.

Demnach sollte eigentlich niemand jemals new Number() verwenden. Aber kürzlich sah ich mich mit folgender API eines Third-Party-Moduls konfrontiert:

connectToServer({
  reconnectionAttempts: <number>
});

Mein Ziel war, einmal einen Verbindungsversuch zum Server zu unternehmen und nach erstmaligem Fehlschlagen sofort aufzugeben, um die Applikation in den permanenten Offline-Modus zu versetzen. Ich wollte also genau 0 reconnectionAttempts haben. Die Implementierung der connectToServer()-Funktion sah allerdings so aus:

export default function connectToServer(options){
  if(!options.reconnectionAttempts){
    options.reconnectionAttempts = 9000;
  }
  ...
}

Dieser Code lässt 0 reconnectionAttempts nicht zu! Der Wert 0 wäre falsy, was dazu führen würde, dass die Bedingung if(!options.reconnectionAttempts) zutrifft und reconnectionAttempts mit irgendeinem sehr hohen Standardwert ersetzt wird:

connectToServer({
  reconnectionAttempts: 0 // tatsächlich 9001 Verbindungsversuche
});

Das ist natürlich ein Bug im Code der connectToServer()-Funktion. Bis dieser repariert ist, ist new Number(0) eine gute Zwischenlösung, denn das Wrapper-Objekt ist truthy, taugt aber trotzdem auch als Zahl 0. Wo immer etwas zahlenhaftes mit einem Number-Objekt unternommen wird (Addition, nicht-strikter Vergleich usw.) wird die gewrappte Zahl aus dem Objekt mittels valueOf() „ausgepackt“.

connectToServer({
  reconnectionAttempts: new Number(0) // Wirklich kein zweiter Verbindungsversuch!
});

Die Nachteile des Wrapper-Objekts werden an dieser Stelle zum Vorteil! Es könnten freilich auch Dinge schiefgehen – ein strikter Vergleich new Number(0) === 0 wäre z.B. false, aber für das, was ich mit besagter Library vorhabe, funktioniert es.

Als bizarres Extra kommt in meinem Fall noch hinzu, dass ich TypeScript statt JavaScript verwende und das TS-Typsystem (berechtigterweise) ein Problem damit hat, wenn man ein Number-Objekt dorthin schiebt, wo ein Number-Primitive erwartet wird. Also ergänze ich meinem Code noch durch eine Type Assertion, die dem Typsystem einredet, das Number-Objekt sei ein Number-Primitive. Im Endeffekt sieht der Code schon so aus, als hätte hier jemand unter dem Einfluss potenter Drogen gestanden:

connectToServer({
  // Mit dem Brecheisen wird ein an sich nutzlos-gefährlicher Wrapper
  // um eine 0 in eine API gesteckt, die das alles nicht haben will.
  reconnectionAttempts: new Number(0) as number
});

Bis eines fernen Tages mal die connectToServer()-Funktion repariert ist, ist das meine eigentlich ganz zufriedenstellende Zwischenlösung. Was lernen wir daraus?

  1. Es lohnt sich, auch die bizarreren oder gar gefährlichen Teile einer Programmiersprache gut zu kennen. Sei es, um genau zu wissen, warum man sie nicht verwendet oder, wenn es hart auf hart kommt, um sie einmal im Jahr doch aus dem Keller zu holen um ein ganz bestimmtes Problem zu lösen.
  2. Schräg aussehender Code kann zwei Ursachen haben: entweder weiß jemand überhaupt nicht, was er da schreibt, oder dieser jemand weiß sehr genau Bescheid. Die letztgenannte Variante, so selten sie auch sein mag, darf man bei der Code-Lektüre nie ganz ausschließen.

Bleibt zu hoffen, dass der Bug in der connectToServer()-Funktion bald repariert wird.

Script-Elemente von einem Dokument ins nächste importieren

Dieser Artikel befasst sich mit einem Problem aus den Randbezirken der täglichen Praxisrelevanz, aber da ich daran ganz schön herumtüfteln musste, kann die Nachwelt sich vielleicht nach der Lektüre der nächsten paar Zeilen ein wenig Arbeit ersparen. Das Problem tauchte auf, als ich einen Bug in meiner kleinen Web Component namens html-import bemerkte. Die Komponente verwende ich, um in meinen HTML-basierten Präsentationen eine clientseitige Importfunktion umzusetzen, ohne dass ich JavaScript zu schreiben brauche. In einer gegebenen Präsentation A möchte ich buchstäblich <html-import src="praesentationB.html#SlideIdFoo"> schreiben können, statt Copy & Paste zu betreiben. Die Komponente funktioniert wie folgt:

  1. Die im src-Attribut angegebene URL wird mit fetch() geladen
  2. Der HTML-Content wird in ein neues Dokument geparsed
  3. Entweder der komplette Inhalt des Dokuments oder ein bestimmtes Element werden in das importierende Dokument importiert (document.importNode())
  4. Der importierte Inhalt wird im Dokument nach dem importierenden <html-import>-Element eingehängt

Dazu kommt allerlei Hexerei für ein Promise auf dem <html-import>-Element und rekursive Imports. Und alles funktionierte ganz hervorragend, bis mir auffiel, dass importierte <script>-Elemente im Firefox nicht ausgeführt wurden, während sie in Chrome funktionierten …

Nach längerer Recherche kann ich nur vermuten, dass Firefox ganz einfach aus Sicherheitsgründen streikt. Ich habe beim Durchforsten der HTML5- und DOM-Spezifikationen nicht herausgefunden, ob das vorgeschrieben oder zulässig ist, aber es scheint einfach so zu sein, dass der Firefox das aus einem fremden Dokument importierte Script nicht vertrauenswürdig findet. Was also tun?

Firefox hat ein Problem mit dem importierten Script-Element, aber nicht mit dem Script-Inhalt. Die Lösung besteht also darin, ein neues Script-Element im importierenden Dokument zu erstellen, den Script-Inhalt (mit der text-Property, nicht mit innerHTML) und/oder das src-Attribut vom Original im Fremd-Dokument zu übernehmen und das neue Script nach dem Original im Original-Dokument einzufügen. Dann funktioniert das Script im Firefox, läuft aber in Chrome zweimal (denn dort funktionieren Original und Klon gleichermaßen). Hier gibt es nun zwei Möglichkeiten:

  1. In meinem Fall, in dem ich Inhalt aus einem Fremd-Dokument übernehme, kann ich ganz einfach das Original-Script aus dem Dokument heraushalten. Nach dem Anlegen des Klons wird das Original einfach verworfen statt ins Dokument eingehängt.
  2. Falls das mal nicht so einfach möglich sein sollte, kann man das Original-Script auch deaktivieren. Hierzu einfach als type-Attribut etwas angeben, das nicht auf der HTML5-Liste der Script-MIME-Types steht.

Das neue Script-Element wird asynchron ausgeführt, aber das wäre beim direkten Import des Originals nicht anders.

Wie sich die ganze Angelegenheit in IE und Safari o.Ä. darstellt, habe ich noch nicht getestet. Auffällig fand ich, dass das für mich das erste Mal seit langem war, dass ich eine Frage nicht durch irgendeine Spezifikation zumindest teilweise beantwortet bekommen habe. Normalerweise kann man sich heutzutage darauf verlassen, dass man in HTML5 u.Ä. zumindest so etwas wie einen klar definierten Soll-Zustand vorfindet. Empirische Browser-Forschung ist jedenfalls bei mir zum Ausnahmefall geworden. Das war früher mal gaaanz anders …

Folgt mir!

Kauft mein Zeug!

Cover der HTML5-DVD
Infos auf html5-dvd.de, kaufen bei Amazon

Cover des HTML5-Buchs
Infos auf html5-buch.de, kaufen bei Amazon

Cover des CSS3-Buchs
Infos auf css3-buch.de, kaufen bei Amazon