Effektvolles Zählen mit CSS-Countern und absoluter Positionierung

Veröffentlicht am 5. Mai 2011

Seit nun schon einigen Iterationen meines Blogdesigns sporten die Kommentarboxen die Anzeige der Kommentarnummer als große, aber dezente Hintergrundgrafik:

Über Twitter fragte @isellsoap gerade, wo denn der Artikel sei, in dem erklärt wird wie man sowas macht – und hier kommt er! Es gibt zwei Wege zum Ziel: der eine ist der elegante Weg, der nur CSS verwendet, der andere kommt nicht ohne zusätzliches HTML-Markup aus, funktioniert aber garantiert in allen Browsern.

Der althergebrachte Weg

In beiden Fällen ist es das Ziel, die Zahl in die rechte obere Ecke der Kommentarbox zu verfrachten und alle anderen Inhalte über diese Zahl zu legen. Ausgehend von dem Fall, dass wir alle Browser bedienen wollen, brauchen wir zunächst die Zahl an sich im HTML. In meinem Wordpress-Template ist das einfach ein span-Element:

<span class="num"><?php print $comnum++; ?></span>

Die Positionierung in der rechten oberen Ecke erreichen wir, indem wir dem Kommentar-Container eine relative Position und der Zahl selbst eine absolute Position verpassen – dann nämlich richtet sich die Zahl relativ zu den Maßen des Containers aus. Mit top und right lässt sich das span-Element in die richtige Ecke schieben:

div.comment {
    position:relative;
}
div.comment span.num {
    font-size:10em;
    position:absolute;
    top:-0.125em;
    right:0;
}

Jetzt steht die Zahl an der richtigen Stelle, überlagert aber alle anderen Elemente in der Box. Um das zu reparieren, müssen diese Elemente ebenfalls relativ positioniert werden und einen hohen Z-Index bekommen – denn der bestimmt, wie sich relativ und absolut positionierte Elemente stapeln:

div.comment p, div.comment pre, div.comment h4 {
    position:relative;
    z-index:1337; /* Höher als alle anderen Elemente */
}

Fertig! Das Problem an dieser Lösung ist nur, dass man für eine reine Design-Spielerei ein HTML-Element einbaut. Das muss nicht sein, moderne Browser vorausgesetzt.

Eine reine CSS-Lösung

In CSS2.1 ist das viel zu wenig beachtete Konzept von CSS-Countern spezifiziert, mit dem man CSS zählen lassen kann. Wir erstellen für unsere Zwecke zunächst einen Counter namens kommentare auf dem body-Element:

body {
    counter-reset:kommentare;
}

Die Eigenschaft counter-reset sorgt dafür, dass jedes Mal, wenn ein body-Element vorkommt, der in der Eigenschaft benannte Counter auf 0 gesetzt wird. Hochzählen lassen kann man einen Counter mit der Eigenschaft counter-increment und an seinen Inhalt kommt man mit der counter()-Funktion heran. Diesen Inhalt steckt man in die content-Eigenschaft eines ::after-Pseudoelements, positioniert dieses genau wie das span-Element im vorherigen Beispiel und zack – die reine CSS-Lösung funktioniert!

body {
    counter-reset: kommentare;
}
div {
    position: relative;
}
h1::after {
    content:counter(kommentare);
    counter-increment:kommentare;
    position:absolute;
    top:0;
    right:0;
}
CSS-Counter in Aktion

In Aktion ist das Ganze hier zu sehen. Der Haken an der Geschichte ist natürlich, dass nur modernere Browser CSS-Counter und die Positionierung von mit content generierten Inhalten unterstützen. Ich hatte jetzt keine Zeit für umfassendere Tests, aber in aktuellen Chrome-, Opera- und Firefox-Versionen sowie im IE9 scheint es zu funktionieren. Sofern man damit leben kann, dass fossile Surfprogramme keine Zahlen zu Gesicht bekommen, ist die zweite Methode ganz klar die bessere, denn HTML sollte in Designfragen nie konsultiert werden müssen.

ECMAScript 5, die nächste Version von JavaScript – Teil 3: Property Descriptors, Getter- und Setter-Funktionen

Veröffentlicht am 18. April 2011

So ziemlich alles kann in JavaScript jederzeit verändert werden. Man kann einem Objekt zu jedem Zeitpunkt neue Eigenschaften verpassen, bestehenden Eigenschaften beliebige neue Werte zuweisen und auf alles zugreifen darf man sowieso. Möchte man nur möglichst schnell ein kleines Script zusammenklöppeln (wofür JavaScript ja ursprünglich gedacht war), ist das sehr praktisch, nicht jedoch, wenn man jedoch dickere Bretter bohrt. Hat man eine viele hundert Zeilen lange JS-Applikation ist es wünschenswert, sich im Sinne der Programmrobustheit gegen versehentliche Mutationen eines Objekts zu versichern. Hier helfen die in ES5 frisch eingeführten Möglichkeiten, Objekte ganz oder in Teilen gegen Veränderungen abzusichern. Und die Getter- und Setter-Funktionen, die ES5 als Bonus mitbringt, sind auch ganz nützlich.

Warum sollte ich meine Objekte nicht mehr modifizieren können wollen?

Eine neue Objekteigenschaft legt man in JavaScript seit jeher in Form eines eleganten Einzeilers an:

var huhn = {};  // Neues Objekt "Huhn"
huhn.eier = 42; // Neue Eigenschaft "eier" für das Objekt "Huhn"
huhn.alter = 3; // Neue Eigenschaft "alter" für das Objekt "Huhn"

Das funktioniert so auch weiterhin in ES5. Ebenso einfach kann man Eigenschaften mit dem delete-Operator wieder aus einem Objekt entfernen:

var huhn = {};
delete huhn.eier;
delete huhn.alter; // Hm...

Nun ist ja ein Huhn ohne Eier durchaus vorstellbar, aber ein lebendiges, gesundes Huhn sollte eigentlich immer ein Alter haben. Ein Programmierer einer Huhn-Applikation würde aller Wahrscheinlichkeit nach davon ausgehen, dass ein huhn.alter immer vorhanden ist. Wenn nun aber irgendwann im Programm huhn.alter aufgrund eines Programmierfehlers verloren geht …

// Löscht bis auf "alter" alle Eigenschaften aus einem Huhn
// Wer findet den Bug?
function clean_huhn(huhn){
    for(var prop in huhn){
        if(prop !== 'allter' && typeof huhn[prop] != 'undefined'){
            delete huhn[prop];
        }
    }
    return huhn;
}

huhn = clean_huhn(huhn);
var doppeltes_alter = huhn.alter * 2; // NaN! Autsch...

… kann das zu extrem ekelhaften Bugs führen. Selbst wenn irgendwann klar wird, dass die Quelle eines Fehlers in der alter-Eigenschaft eines Huhns zu suchen ist (was durchaus nicht immer offensichtlich sein muss!) weiß man immer noch nicht, wo im Programm alter verloren geht. Wäre es da nicht praktisch, wenn man kritische Objekteigenschaften so absichern könnte, dass allein der Versuch sie zu löschen mit einer lauten Fehlermeldung fehlschlägt? Die in ECMAScript 5 eingeführten Property Descriptors helfen dabei und bei vielem mehr.

Robuste JavaScript-Objekte mit property descriptor maps

Das in ES5 neu eingeführte Object.defineProperty() definiert eine Eigenschaft auf einem Objekt, in etwa wie huhn.alter = 3. Der Clou ist, dass man dieser Eigenschaft ein Konfigurationsobjekt mitgeben kann, das bestimmt, wie sich die neue Objekt-Eigenschaft verhält. Das Ganze funktioniert, indem man Object.defineProperty() neben dem betroffenen Objekt und dem Namen der zu definierenden Eigenschaft eine sogenannte property descriptor map übergibt – einfach ein weiteres Objekt, in dem neben dem Wert der Eigenschaft die Einstellungen writable, configurable und enumerable festgelegt werden:

var huhn = {};  // Neues Objekt "Huhn"
Object.defineProperty(huhn, 'alter', {
    writable: true,      // Wert KANN verändert werden
    enumerable: true,    // DARF in for-in-Schleifen auftauchen
    configurable: false, // DARF NICHT gelöscht werden
    value: 3             // Wert von "alter" ist 3
});

Dieser Code entspricht in etwa huhn.alter = 3, legt aber über die property descriptor map neben Namen und Wert der Eigenschaft auch fest, dass sie nicht gelöscht werden kann; configurable steht auf false. Die drei Eigenschaften in der property descriptor map haben folgende Funktionen:

  • writable; wenn false, kann der Wert der betroffene Objekt-Eigenschaft nicht mehr verändert werden
  • enumerable; wenn false, taucht die betroffene Objekt-Eigenschaft nicht mehr in den Auflistungen aller Eigenschaften des Objekts (wie z.B. bei for-in-Schleifen) auf
  • configurable; wenn false, kann die Eigenschaft nicht gelöscht werden und auch seine writeable- enumerable- und configurable-Werte können nicht mehr verändert werden

Die mit einem configurable-Wert von false abgesicherte alter-Eigenschaft sorgt in unserem Beispiel mit der clean_huhn()-Funktion dafür, dass alter nicht mehr gelöscht wird, komme was wolle. Im Strict Mode wird dort, wo das Problem entsteht (eine Objekteigenschaft, die nicht gelöscht werden kann, wird versucht zu löschen) vom Browser sogar eine handfeste Fehlermeldung ausgespuckt:

"use strict";
var huhn = {};

// Neue _unlöschbare_ Eigenschaft "alter" für das Objekt "Huhn"
Object.defineProperty(huhn, 'alter', {
    writable: true,      // Wert KANN verändert werden
    enumerable: true,    // DARF in for-in-Schleifen auftauchen
    configurable: false, // DARF NICHT gelöscht werden
    value: 3             // Wert von "alter" ist 3
});

// Löscht bis auf "alter" alle Eigenschaften aus einem Huhn
// Wer findet den Bug?
function clean_huhn(huhn){
    for(var prop in huhn){
        if(prop !== 'allter' && typeof huhn[prop] != 'undefined'){
            delete huhn[prop]; // FEHLER: property is non-configurable and can't be deleted
        }
    }
    return huhn;
}

huhn = clean_huhn(huhn);
var doppeltes_alter = huhn.alter * 2; // So weit kommt das Programm gar nicht

So kriegt man erstens sicher mit, dass es überhaupt einen Programmfehler gibt und zweitens bekommt man auch gleich den wahren Quell des Problems geliefert. Theoretisch könnte man das konkrete Problem im Beispiel auch dadurch lösen, dass man enumerable auf false setzt – alter würde dann gar nicht erst in der for-in-Schleife auftauchen.

Übrigens: alle drei Werte der property descriptor map sind standardmäßig false, so dass man eine komplett gegen Veränderungen gesicherte Objekteigenschaft bequem in einer Zeile definieren kann:

// Die Secret ID ist unsichtbar und unveränderlich
Object.defineProperty(huhn, 'secretid', { value: 'H2045' });

Wer gleich mehrere Objekteigenschaften auf einen Schlag anlegen möchte, hat hierfür Object.defineProperties zur Verfügung. Einfach als zweites Argument ein Objekt voller property descriptor maps angeben, fertig:

Object.defineProperties(huhn, {
    'eier': {
        writable: true,
        enumerable: true,
        configurable: true,
        value: 42
    }
    'alter': {
        writable: true,
        enumerable: true,
        configurable: false,
        value: 3
    }
});

Möchte man die property descriptor map einer Objekteigenschaft abfragen, so geht dies mit Object.getOwnPropertyDescriptor:

// Gibt die komplette property descriptor map als Objekt zurück
console.log(Object.getOwnPropertyDescriptor(huhn, 'alter'));

Einen Haufen Beispiele für die Benutzung von Object.defineProperty() zum herumspielen und forken gibt es bei jsFiddle.

Ganze Objekte absichern

Mit property descriptor maps kann man einzelne Objekteigenschaften gegen ungewollte Veränderungen oder zu viel Sichtbarkeit absichern, doch auch für ganze Objekte bietet ES5 vergleichbare Funktionen. So kann man mit Object.preventExtensions() dafür sorgen, dass einem Objekt keine neuen Eigenschaften hinzugefügt werden können. Diese Aktion ist endgültig; ein nicht erweiterbare Objekt kann nicht wieder in ein erweiterbares Objekt verwandelt werden. Mit Object.isExtensible() kann man überprüfen, ob ein Objekt erweiterbar ist:

var huhn = {};
huhn.eier = 42
Object.preventExtensions(huhn);

huhn.alter = 3    // Funktioniert nicht
delete huhn.eier; // Funktioniert

Ein gegen Erweiterungen geschütztes Huhn ist wirklich nur vor Erweiterungen sicher, andere Operationen wie das Löschen von bereits vorhandenen Objekteigenschaften funktionieren weiterhin. Soll auch das verhindert werden, hilft Object.seal(). Ein so behandeltes Objekt kann nicht nur keine neuen Erweiterungen erhalten, sondern seine sämtlichen vorhandenen Eigenschaften erhalten obendrein einen configureable-Wert von false:

var huhn = {};
huhn.eier = 42
Object.seal(huhn);

huhn.alter = 3    // Funktioniert nicht
delete huhn.eier; // Funktioniert nicht

Ob ein Objekt mit Object.seal() behandelt wurde, lässt sich via Object.isSealed() ermitteln und wie schon bei Object.preventExtensions() gibt es keine Möglichkeit, eine Sperre rückgängig zu machen. Mit Object.freeze() lassen sich Objekte letztlich komplett gegen jede Art von Veränderungen absichern. Weder können neue Eigenschaften eingebaut oder gelöscht werden, noch lassen sich die property descriptor maps der vorhandenen Eigenschaften verändern:

var huhn = {};
Object.defineProperty(huhn, 'eier', {
    value: 42,
    writable: true
});
Object.freeze(huhn);

huhn.alter = 3       // Funktioniert nicht
delete huhn.eier;    // Funktioniert nicht
Object.defineProperty(huhn, 'eier', {
    writable: false  // Funktioniert nicht
});

Ob ein Objekt via Object.freeze() eingefroren wurde, lässt sich via Object.isFrozen() ermitteln.

Getter- und Setter-Funktionen

Das allseits bekannte DOM-Node-Eigenschaft innerHTML ist ein seltsamer Geselle. Sie ist eine normales Objekt-Eigenschaft, doch ihr Verhalten ähnelt eher einer Funktion – denn schließlich passiert etwas, wenn man ihren Wert modifiziert (das DOM der Webseite verändert sich). Eine Änderung des Wertes einer Eigenschaft löst also eine Funktion aus. Mit althergebrachtem JavaScript kann man derartiges Verhalten nicht programmieren, Sonderlinge wie innerHTML sind in C++ fest in die Browser einprogrammiert. Rettung naht in Form der in ES5 eingeführten Getter- und Setter-Methoden.

Getter- und Setter-Methoden lassen sich definieren, indem man sie mit den Schlüsseln get bzw. set in die property descriptor map einer Objekteigenschaft hineinschreibt:

var huhn = {};

Object.defineProperty(huhn, 'eier', {
    set: function(x){
        alert('Gacker!');
    }
});

huhn.eier = 3; // Löst das "Gacker"-Alert aus

Wenn Ein Getter oder ein Setter definiert sind, darf die property descriptor map weder einen einen Startwert noch einen writeable-Wert definieren! Der Grund: Objekteigenschaften mit Getter- und Setter-Methoden repräsentieren selbst keine Werte in einem Objekt sondern sind nur Pseudo-Eigenschaften, die andere Objekt-Eigenschaften verändern können. Der obrige Code löst zwar, wenn wir huhn.eier auf 3 setzen, die Setter-Methode aus, macht aber mit dem gesetzten Wert (Funktionsargument x) nichts. Würden wir huhn.eier abfragen, bekämen wir undefined zurück – huhn.eier selbst hat keinen Wert (da es nur eine Pseudo-Eigenschaft ist) und weil wir keine Getter-Funktion definiert haben, kann huhn.eier auch nichts anderes zurückgeben.

Eine Umsetzung von huhn.eier, die auch tatsächlich funktioniert, könnte so aussehen:

var huhn = {};

// Die wahre Anzahl der Eier
Object.defineProperty(huhn, 'eierImNest', { writable: true, value: 42 });

// Eier-Pseudo-Eigenschaft leitet alles auf this.eierImNest um
Object.defineProperty(huhn, 'eier', {
    get: function(){
        return this.eierImNest;
    },
    set: function(x){
        this.eierImNest = x;
    }
});

console.log(huhn.eier); // 42
huhn.eier = 50;
console.log(huhn.eier); // 50
huhn.eier++;
console.log(huhn.eier); // 51

Die wahre Anzahl der Eier liegt in der Objekteigenschaft eierImNest, die durch den Setter von eier gesetzt und durch den Getter von eier zurückgegeben wird. Nutzlos? Absolut nicht! Es ist ja nicht gesagt, dass Getter und Setter nicht noch mehr machen dürfen. Wie wäre es zum Beispiel mit einem eingebauten Profiler für unser Huhn, der alle Zugriffe auf eier mitzählt?

var huhn = {};

// Die wahre Anzahl der Eier
Object.defineProperty(huhn, 'eierImNest', { writable: true, value: 42 });

// Die Anzahl der Eier-Zugriffe
Object.defineProperty(huhn, 'log', { writable: true, value: 0 });

// Eier-Pseudo-Eigenschaft leitet alles auf this.eierImNest um
Object.defineProperty(huhn, 'eier', {
    get: function(){
        this.log++; // Zugriff zählen
        return this.eierImNest;
    },
    set: function(x){
        this.log++; // Zugriff zählen
        this.eierImNest = x;
    }
});

console.log(huhn.eier); // 42
huhn.eier = 50;
console.log(huhn.eier); // 50
huhn.eier++;
console.log(huhn.eier); // 51

console.log(huhn.log);  // 6 (huhn.eier++ sind zwei Operationen)

Mit ein bisschen Trickserei können wir die echten Objekteigenschaften auch komplett aus unserer Huhn-API entfernen und Zugriff ausschließlich über die Getter- und Setter-Methoden der Pseudoeigenschaften erlauben:

var Huhn = function(){
    var that = this;

    // Die wahre Anzahl der Eier
    Object.defineProperty(this, 'eierImNest', { writable: true, value: 42 });

    // Die Anzahl der Eier-Zugriffe
    Object.defineProperty(this, 'log', { writable: true, value: 0 });

    // Die API, die wir zurückgeben besteht nur aus Pseudo-Eigenschaften
    var api = {}

    // Eier-Pseudo-Eigenschaft leitet alles auf this.eierImNest um
    Object.defineProperty(api, 'eier', {
        get: function(){
            that.log++;
            return that.eierImNest;
        },
        set: function(x){
            that.log++;
            that.eierImNest = x;
        }
    });

    // Pseudo-Eigenschaft zur Abfrage des Logs. Kein Setter!
    Object.defineProperty(api, 'log', {
        get: function(){
            return that.log;
        }
    });

    // Nur die API zurückgeben
    return api;

}

var huhn = new Huhn();

console.log(huhn.eier); // 42
huhn.eier = 50;
console.log(huhn.eier); // 50
huhn.eier++;
console.log(huhn.eier); // 51

console.log(huhn.log);  // 6 (huhn.eier++ sind zwei Operationen)

Die echten Objekteigenschaften sind gegen unbefugten Zugriff von außen sicher und der Profiler loggt alles mit. Trotzdem ist für den Benutzer der Huhn-API alles, als würde ein ganz normales JavaScript-Objekt bearbeiten. Kleiner Tipp am Rande: Getter und Setter kann man auch ohne property descriptor map definieren, indem man sie einfach in das Objekt hineinschreibt. Wichtig ist das Auslassen des Doppelpunktes nach get und set:

var huhn = {
    eierImNest: 42,
    log: 0,
    get eier(){
        this.log++;
        return this.eierImNest;
    },
    set eier(x){
        this.log++;
        this.eierImNest = x;
    }
};

Das ist erheblich weniger Tipperei als alles bisher gesehene, hat aber den Nachteil, dass das Objekt nach althergebrachter Manier beliebig manipulierbar ist. Drückt man sich etwas kompakter aus und nutzt einige der bisher noch nicht vorgestellten ES5-Helferlein (die kommen im nächsten Artikel dran), kann man die robuste Variante des Profiler-Huhns auch mit etwas weniger Tastenabnutzung umsetzen:

var Huhn = function(){
    var that = this;
    Object.defineProperties(this, {
        'eierImNest': { writable: true, value: 42 },
        'log': { writable: true, value: 0 }
    });
    return Object.create(Object.prototype, {
        'eier': {
            get: function(){
                that.log++;
                return that.eierImNest;
            },
            set: function(x){
                that.log++;
                that.eierImNest = x;
            }
        },
        'log': {
            get: function(){
                return that.log;
            }
        }
    });
}

Immer noch recht viel Code, aber hey: ein Objekt mit eingebautem Profiler! Wer will da meckern?

Fazit

Manipulationssichere Objekte und Objekteigenschaften sind zusammen mit den Getter- und Setter-Methoden eine schöne Ergänzung des im letzten Teil der Artikelserie vorgestellten Strict Mode. Der Strict Mode definiert eine Browser-Umgebung, die es schwieriger macht, sich mit schlechtem Code in Schwierigkeiten zu bringen. Passend dazu ermöglichen es die hier vorgestellten ES5-Features, eigene APIs so zu programmieren, dass man sich bei ihrer Benutzung nicht unwissentlich ins eigene Knie schießt. Nützlich ist das Ganze daher eher Webnerds, die auch selbst API-Autoren sind – Otto Normal-DOM-Node-Schubser wird von diesen Features eher passiv profitieren. Wenn jQuery und Co robuster und einfacher werden, haben auch jene etwas davon, die nie selbst Object.defineProperty schreiben werden.

Die allmächtige Kompatibilitätstabelle zeigt mit Opera und Safari zwei Browser, deren aktuellste Versionen die vorgestellten Funktionen nicht voll bis gar nicht unterstützen und die sich damit in Gesellschaft der Internet Explorer 6 bis 8 begeben. Mit Polyfills kann man zwar nicht alle Funktionen, wohl aber die meisten APIs rund um eingefrorene Objekte, Property Descriptors sowie Getter- und Setter-Funktionen nachrüsten, was in diesem Fall schon die halbe Miete ist: denn seinen Code testen kann man ja in einem Browser mit echter ES5-Unterstützung.

ECMAScript 5, die nächste Version von JavaScript – Teil 2: Der Strict Mode

Veröffentlicht am 12. April 2011

Dass das alte Testament der modernen JavaScript-Community den Titel JavaScript: The Good Parts trägt, lässt erahnen, dass nicht alles in dieser Programmiersprache ganz problemfrei ist, also auch einiges an bad parts vorhanden ist. Diese werden von geübten Programmieren meist gemieden, stellen aber immer noch eine veritable Tretmine dar. Um diese problematischen „Features“ langsam gen Altenteil zu schieben, führt ES5 den Strict Mode ein, der eigentlich nichts weiter als ein Subset von „normalem“ JavaScript ist – alles Gute bleibt drin, alles Böse existiert im Strict Mode nicht mehr. Und wer weiter böse programmieren will, muss sich nicht fürchten, denn der Strict Mode ist ein Opt-In-Feature.

Was macht der Strict Mode?

Der Strict Mode ist ein bereinigtes JavaScript-Subset. Er führt nichts neues ein, entfernt aber einige problemtische Altlasten. Unter anderem wird

  • das unberechenbare with-Statement abgeschafft,
  • ein Fehler ausgelöst wenn man durch ein vergessenes var unbeabsichtigt eine globale Variable anlegt,
  • die Anzahl der befremdlichen Eigenheiten von this, eval() und arguments reduziert,
  • durch unsinnige Anweisungen wie NaN = 42 ein Fehler ausgelöst
  • und die oktale Syntax (0644 == 420) verboten.

JavaScript wird im Strict Mode also entrümpelt und vieles, was früher nur bad practice war, in einen handfesten Fehler, den ein Browser auch meldet. Es ist im Strict Mode einfach wesentlich schwieriger, schlechte Programme zu scheiben: Fehler werden sichtbarerer, die Quellen für solche Fehler werden weniger und der Programmierer kann sich mehr auf seine Aufgabe als auf die Besonderheiten seiner Programmiersprache konzentrieren.

Den Strict Mode auslösen

Strict Mode auszulösen ist einfach; man braucht nur "use strict"; an den Anfang einer Scriptdatei zu setzen:

"use strict";
x = "foo"; // ReferenceError - "var" vergessen

Das Ganze funktioniert auch auf Funktionsebene. Wer nicht komplette Scripts in den Strict Mode versetzen will, kann sich auf einzelne Funktionen beschränken:

var x = "Ich bin NICHT im Strict Mode!";

function foo(){
    "use strict";
    var y = "Ich BIN im Strict Mode!";
}

function bar(){
    var z = "Ich bin NICHT im Strict Mode!";
}

Der Strict Mode ist an den Funktionskontext gebunden. So wie innerhalb einer Funktion erstellte Funktionen auf die Variablen der äußeren Funktion Zugriff haben, so erben sie auch den Strict Mode:

var x = "Ich bin NICHT im Strict Mode!";

function foo(){
    "use strict";
    var y = "Ich BIN im Strict Mode!";
    return function(){
        var z = "Ich bin AUCH im Strict Mode!";
    }
}

Zu beachten ist, dass man den Strict Mode nicht auf einzelne If- oder Schleifen-Blöcke beschränken kann. Das Ganze funktioniert nur auf Script- oder Funktionsebene.

Strict-Mode-Scripts sind zu 100% abwärtskompatibel. Da für Browser, die den Strict Mode nicht kennen, "use strict"; nur ein beliebiger String, also ein JavaScript-Ausdruck ohne Bedeutung ist, verarbeitet auch der älteste Internet Explorer solche Scripts ohne zu murren – der Strict Mode definiert schließlich nichts weiter als ein Subset von „normalem“ JavaScript. Man kann also (theoretisch) ohne Probleme ab heute dazu übergehen, nur noch im Strict Mode zu programmieren, vorausgesetzt man testet seine Scripts vorher in mindestens einem Browser, der dieses ES5-Feature auch implementiert.

Die Auswirkungen des Strict Mode im Detail

Weniger globale Variablen: Erstellt man in normalem JavaScript eine neue Variable, ohne ihr var voranzustellen, macht man sie zu einer globalen Variable. Da dieser Effekt fast nie so gewollt ist (wer globale Variablen braucht, sollte sie auf der globalen Ebene mit var entsprechend deklarieren) und meist auf ein Vergessen des var zurückzuführen ist, hagelt es hierfür im Strict Mode Fehler. So kann man sich sicher sein, dass wenn am Ende des Tages das Programm keine Fehler wirft, man nirgends ein var vergessen hat.

"use strict";
x = 42; // ReferenceError - "var" fehlt

Mehr (sichtbare) Fehler: Es gibt in JavaScript viele Dinge, die man zwar ausdrücken kann, die aber nicht funktionieren. Es ist absolut möglich undefined = 42 in ein Script zu schreiben, doch einen Effekt hätte es (zum Glück) nicht – undefined ist (in ES5) schreibgeschützt. Damit man es aber auch mitbekommt, wenn man solcherlei Unsinn schreibt, wirft JavaScript im Strict Mode immer dort einen Fehler, wo es vorher nur stillschweigendes Nichtfunktionieren gab:

"use strict";
undefined = 42;          // TypeError - "undefined" kann nur undefined sein
delete Object.prototype; // TypeError - Object.prototype ist schreibgeschützt

Mehr Einzigartigkeit: Verwendet man in einer Funktion mehrere Argumente mit gleichem Namen oder versucht man, in einem Objektliteral mehrere gleichnamige Eigenschaften zu definieren, setzt es im Strict Mode einen Syntaxfehler. Nur so bemerkt man solche einfache Fehler sofort beim ersten Test, denn ohne Strict Mode würde einfach das jeweils letzte Argument bzw. die letzte Objekteigenschaft stillschweigend ihre Vorgänger überschreiben. Und so etwas zu debuggen kann richtig schwierig werden – schwieriger jedenfalls, als einfach die Syntaxfehler im Strict Mode zu finden und auszumerzen.

"use strict";
var o = { foo: "x", foo: "y" }; // SyntaxError - Zwei mal "foo" geht nicht
function foo(x, x, z){}         // SyntaxError - Zwei mal "x" geht nicht

Weniger WTF bei eval() und arguments:

Im Strict Mode sind eval und arguments (enthält in Funktionen alle Argumente dieser Funktion) reservierte Namen, die nicht überschrieben werden können. Das auf die aufrufende Funktion verweisende arguments.callee ist aufgrund seiner Arbeitsweise ein großes Hindernis für die Performanceoptimierung-Mechanismen in den Browsern und wird unter anderem deshalb abgeschafft. Außerdem sind die Argumente einer Funktion nicht mehr fest an arguments gebunden – arguments enthält im Strict Mode immer die Original-Werte, mit denen die Funktion ursprünglich aufgerufen wurde, Modifikationen werden nicht mehr übertragen:

function foo(x){
    x++;
    console.log(arguments[0]); // 43
}
foo(42);

function bar(x){
    "use strict";
    x++;
    console.log(arguments[0]); // 42 dank Strict Mode
}
bar(42);

Weniger WTF bei this: Bei Funktionsaufrufen wie foo(); ist in normalem JS this in der Funktion das globale Objekt. Wenn man in dieser Situation versucht, Methoden und Eigenschaften für this zu definieren (weil man sich zum Beispiel aufgrund eine Programmierfehlers fälschlicherweise im Methoden- oder Constuctorkontext wähnt), produziert man globale Variablen ‐ und zwar ohne es zu merken! Deshalb gilt im Strict Mode: this ist in Funktionsaufrufen undefined, so dass Irrtümer wie ein vergessenes new schneller auffallen – undefined kann man schließlich nicht mit neuen Eigenschaften oder Methoden versehen, ohne dass es Fehler hagelt.

Weniger Freiheiten bei Funktionsstatements: Funktionsstatements wie function foo(){} erlauben moderne Browser an so ziemlich jeder Stelle im Code, kommen aber in If-Blöcken oder Schleifen zu sehr unterschiedlichen Ergebnissen bei der Auswertung dieser Statements. Um dieses Problem in aus dem Weg zu räumen, dürfen im Strict Mode Funktionsstatements nur noch direkt in einem Script oder einer Funktion stehen, nicht innerhalb von If-Blöcken oder Schleifen:

"use strict";
if(true){
    function foo(){} // SyntaxError
}

function bar(){}     // Kein Problem

Weniger with-Statement: Niemand sollte je das with-Statement verwenden, also verbietet es der Strict Mode und erklärt es zu einem Syntaxfehler.

Weniger oktale Syntax: Alle Browser interpretieren Zahlen mit vorangestellter Null als Oktalzahl (0644 == 420) – und das, obwohl es gar nicht im ECMAScript-Standard vorgeschrieben ist. Da Oktalzahlen selten nützlich und oft verwirrend sind, sind sie im Strict Mode verboten und produzieren einen Syntaxfehler.

Mehr reservierte Wörter: implements, interface, let, package, private, protected, public, static, und yield sind im Strict Mode reservierte Namen für zukünftige ECMAScript-Versionen und können nicht mehr als Variablennamen u.Ä. verwendet werden:

"use strict";
var package = 42; // package is a reserved identifier

Fazit und Fallen

Der Strict Mode ist nicht viel mehr, als ein um einige Fallen bereinigtes JavaScript. Da er ein Subset von „normalem“ JavaScript darstellt sollten Strict-Mode-Scripts problemlos in jedem modernen und unmodernen Browser funktionieren. Neu ist eigentlich nur, dass man im Strict Mode so programmiert, als würde einem ständig Douglas Crockford über die Schulter schauen.

Die einzig große Falle könnte dann zuschnappen, wenn performancebedingt mehrere JS-Dateien zusammenfügt werden; ist nur ein einziges Strict-Mode-Script dabei, wird der gesamte Code nach den strengen Regeln ausgewertet, was, wenn nicht alle Scripts hierfür vorbereitet sind, übel enden könnte. Als Vorsichtsmaßnahme kann man, wenn man es nicht ohnehin schon macht, seine Scripts in eine sofort ausgeführten Funktion kapseln. So kann man sich sicher sein, dass der Strict Mode immer auf das eigene Script beschränkt bleibt:

(function(){
    "use strict";
    // Code
})()

Sollte man schon heute den Strict Mode verwenden? Schwierige Frage. Einerseits müsste, wie bereits erwähnt, jeder Browser Strict-Mode-Code ausführen – nur wird eben nicht jeder Browser die diversen Neuheiten durchsetzen. Da es aber zu Zeit mit dem Firefox 4 nur einen einzigen Browser gibt, der den Strict Mode (vollständig) implementiert, hat man als Webentwickler nur diesen einen Browser als Testplattform zur Verfügung. Und nun stelle man sich vor, es gäbe einen Bug im Strict Mode des Firefox 4, der Code, der eigentlich Fehler auslösen müsste, passieren lässt – und dann updaten eines Tages alle anderen Browserhersteller ihre Produkte mit einem korrekten Strict Mode und das nur im Firefox getestete Script funktioniert nicht mehr …

Für den Moment ist es vielleicht nicht die schlechteste Idee, Scripts im Strict Mode zu entwickeln, das "use strict"; aber zu entfernen bevor man den Code in die freie Wildbahn loslässt. So nutzt man beim Programmieren alle Vorteile, hat aber am Ende mit absoluter Sicherheit ein funktionierendes Programm – und das ist ja auch nicht ganz unwichtig.

ECMAScript 5, die nächste Version von JavaScript – Teil 1: Ein Überblick

Veröffentlicht am 5. April 2011

Heimlich, still und leise hält die nächste Version von JavaScript Einzug in unsere Browser. Überschattet von Trubel um HTML5 und CSS3 merkt fast keiner, dass neben HTML und CSS auch das oft so verkannte JavaScript dabei ist, ein größeres Update zu erfahren und dass sich somit alle drei Grundpfeiler des Webs – HTML, CSS und eben JavaScript – weiterentwickeln. Dieser Artikel soll der Startschuss zu einer kleinen Serie sein, in der wir einen Blick auf ECMAScript 5 (oder kurz ES5) und damit die nähere Zukunft von JavaScript wagen werden. Besonderes Augenmerk werden wir natürlich darauf richten, in welchem Umfang die verschiedenen Neuerungen schon zu gebrauchen sind, doch unser erster Blick gilt den Hintergründe von ES5. Was ist eigentlich „ECMAScript“ und wieso hat es die Versionsnummer 5?

Was ist ECMAScript und wieso gibt es Version 5?

Als Anfang der 90er Jahre Brendan Eich für Netscape innerhalb weniger Tage eine kleine Browser-Programmiersprache zusammenschraubte, grassierte der Java-Wahn. Man war sich damals so sicher, dass Java die wichtigste Programmiersprache der Zukunft sein würde, dass man aus Marketinggründen entschied, die neue Browser-Programmiersprache JavaScript zu nennen – obwohl beide Technologien sich auch nicht nur im entferntesten ähneln. Dass man damit bis in alle Ewigkeit Verwirrung bei Webentwicklern stiften würde, war weniger wichtig, als dass man dort war, wo man damals „vorne“ vermutete.

JavaScript had to look like Java only less so, be Java’s dumb kid brother or boy-hostage sidekick.

 – Brendan Eich

Seither hat sich viel getan. Java hat überraschenderweise nicht die Weltherrschaft übernommen und stattdessen hat sich ausgerechnet JavaScript von einer Spielerei zur wichtigsten Programmiersprache der Welt verwandelt. Aufgrund des Namens-Deals, den Netscape seinerzeit mit dem Java-Entwickler Sun Microsystems eingefädelt hatte, liegen die Namensrechte heutzutage beim momentanen Sun-Besitzer Oracle, während die Spezifikationen der Sprache von einer privaten Normierungsorganisation namens Ecma International (ehemals ECMA – European Computer Manufacturers Association) gepflegt werden. Das, was wir alle als JavaScript kennen und bezeichnen, ist also eine Implementierung eines Standards namens ECMAScript bzw. ECMA-262.

Die letzte von allen heute verbreiteten Browsern unterstützte Fassung des Standards ist die dritte Edition von ECMA-262, die im November 2000 verabschiedet wurde. Die Pläne zu einer vierten Edition zerschellten unvollendet an ihren eigenen Ansprüchen – man wollte zu viel, konnte sich nicht einigen und stellte schließlich die Arbeit an der vierten Edition ein. So kam es, dass jetzt mit der fünften Fassung ein vergleichsweise bescheidenes Update im Anflug ist, ohne dass es je eine vierte gegeben hätte. Erklärtes Ziel von ECMAScript 5: die Sprache entrümpeln und robuster machen, damit man in Zukunft ein gutes Fundament für dann wieder hochtrabendere Pläne hat.

Was kann die neue Version?

Dafür, dass Brendan Eich nur wenige Tage Zeit hatte um JavaScript zu entwickeln und nebenbei unter enormem Druck aus der Netscape-Führungsetage stand, kann sich das Ergebnis sehen lassen.

I had to be done in ten days or something worse than JS would have happened.

 – Brendan Eich

Doch trotzdem haben sich aufgrund der Kürze der Zeit einige sehr sonderbare Eigenschaften in die Sprache eingeschlichen. Eine kleine Auswahl mit Beispielen, die sicher die meisten von uns kennen:

// Ergibt "object" statt "array"
typeof [];

// "bar" wird eine _globale_ Variable, weil das "var" vergessen wurde
function foo(){
    bar = 42;
}

// Ergibt "8"
parseInt('010');

Zwar hat jede Programmiersprache so ihre Macken, doch nur bei wenigen füllen die WTF-Momente ganze Websites und sogar Bücher. Oberstes Ziel von ES5 ist es, die Anzahl der genannten WTF-Momente zu reduzieren. Außerdem soll die Rubustheit von JavaScript erhöht werden – die Zeiten, als jedes Objekt jederzeit manipuliert werden konnte, sind vorbei. Und auch das Webentwickler-Leben soll durch eine Handvoll neuer Helferlein erleichtert werden. Im Einzelnen sind dabei:

  • Scripte und Teile von Scripts können in einen Strict Mode versetzt werden, in dem einige defekte JS-Altlasten (z.B. with(){}) nicht mehr verwendet werden können und der an einigen Stellen die Regeln von JavaScript so verändert, dass es weniger WTF-Momente gibt.
  • Mittel und Wege, um Objekte gegen Manipulationen zu sichern werden ebenso eingeführt wie Getter- und Setter-Methoden für Objekteigenschaften
  • Viele neue Helferlein lösen ganz alltägliche Probleme und reduzieren den Bedarf an kryptischen Eigenbau-Lösungen und Boilerplate-Code. Genannt seien unter anderem Object.create() (erstellt in einem Arbeitsschritt ein Objekt und legt seinen Prototyp fest), Array.isArray() und Array.prototype.forEach()

Einige dieser Features werden von heutigen JavaScript-Frameworks bereits bereitgestellt, so dass man sich durchaus fragen kann, ob ES5 einen überhaupt kümmern sollte.

Muss mich als jQuery-Nutzer ES5 überhaupt interessieren?

Wer wirklich niemals mehr macht, als via Framework ein paar DOM-Knoten durch die Gegend zu schrieben, kommt vermutlich die nächste Zeit auch ohne detaillierte Kenntnisse der meisten ES5-Features über die Runden. Einiges anderes, wie zum Beispiel die durchaus vorhandenen Gefahren des Strict Mode, sollte man aber in jedem Fall auf dem Schirm haben. Und schließlich gilt: die neuen Features einer Programmiersprache aus einer informierten Perspektive heraus zu ignorieren ist immer besser, als sie aus Gründen der Ignoranz nicht zu verwenden. Ebenfalls zu bedenken ist, dass JavaScript auch außerhalb des Browser mehr und mehr um sich greift. Egal ob Node.js oder Windows-Widgets, so manche Offline-Anwendung ist bereits heute in JS geschrieben. Und es ist nicht damit zu rechnen, dass sich dieser Trend so bald umkehrt – vor allem nicht, wenn die Sprache durch frische Features immer attraktiver wird.

Welche Browser unterstützen ES5 und wie geht es weiter?

JavaScript hat wie jede Browsertechnologie das Problem, dass man nicht einfach so eine neue Version rausbringen kann, die dann innerhalb kürzester Zeit überall verfügbar ist. Allerdings ist bei ES5 die Lage nicht ganz schlimm, wie man dieser Kompatibilitätstabelle entnehmen kann. Der Internet Explorer 8 ist, wie zu erwarten war, der Internet Explorer 6 von ES5, aber alle anderen Browser enthalten schon viele ES5-Features. Insbesondere bei Chrome, Firefox ab Version  4 und Internet Explorer ab Version 9 kann man von sehr guter Unterstützung sprechen. Die Lage stellt sich also rosig genug dar, um eine kleine Artikelserie zu rechtfertigen. Sofern sich genügend freie Zeit auftreiben lässt, werden es in den nächsten Wochen und Tagen noch vier weitere Artikel zum Thema ES5 erscheinen, die Einzelaspekte unter die Lupe nehmen. Wenn alles nach Plan läuft, werden wir in ein paar Tagen als erstes den Strict Mode behandeln – Feedback und Sonderwünsche sind dabei wie immer gern gesehen!