Free email newsletter: “ES.next News

2013-01-27

JavaScripts 12 größte Fallgruben

[Dieser Blogpost ist die Langversion eines Artikels im CHIP Web Design 2013.]

JavaScript ist eigentlich eine recht kompakte Sprache. Wenn es nur nicht all diese Fallgruben gäbe... Dieser Artikel erklärt die 12 größten und wie man am besten mit ihnen umgeht. Zur Lektüre werden grundlegende JavaScript-Kenntnisse vorausgesetzt. Wir halten uns an die aktuelle Version von JavaScript, ECMAScript 5.

Anmerkung: Da Oracle, als Erbe von Sun, ein Trademark auf den Begriff „Java“ hat und dieses nur an Mozilla lizensiert, dürfen nur diese beiden Firmen offiziell den Begriff „JavaScript“ verwenden. Inoffiziell hindert das keinen, die Sprache so zu nennen, aber für den offiziellen Sprachstandard musste ein anderer Name gefunden werden. Dieser Name ist ECMAScript, nach der Organisation Ecma, die den Standard verwaltet. Die aktuelle Version von JavaScript ist ECMAScript 5 und existiert seit Dezember 2009, die nächste Version, ECMAScript 6, wird Ende 2013 fertig werden. Komplette Unterstützung wird danach etwas dauern, aber Teile wird man schon davor ausprobieren können.

Die folgenden Abschnitte behandeln je eine Fallgrube. Am Ende wird ein Ausblick auf ECMAScript 6 gegeben, das viele der Fallgruben eliminiert.

Fallgrube: implizite Umwandlungen von Werten

JavaScript ist sehr tolerant beim Annehmen von Werten. Überall wo z.B. eine Zahl erwartet wird, weist die Sprache Werte anderer Typen nicht zurück, sondern versucht sie in Zahlen umzuwandeln. Zum Beispiel:
    > '5' - '2'
    3
    > '5' * '2'
    10
Der Plus-Operator (+) funktioniert etwas anders und kann sowohl mit Zahlen als auch mit Zeichenketten umgehen. Das führt zu einer anderen Art von Problem, wie wir gleich sehen werden.

Das automatische Umwandeln nach Boolean ist meistens eher praktisch, wir befassen uns aber damit, weil es Wissensgrundlage für spätere Themen ist. Eine echte Fallgrube ist hingegen, wie Werte von String-Werte umgewandelt.

Implizite Umwandlung nach Boolean: „truthy“ und „falsy“ Werte

Wenn Wahrheitswerte erwartet werden, z.B. für die Bedingung einer if-Anweisung, kann man beliebige Werte übergeben, die entweder als true oder false interpretiert werden. Die folgenden Werte werden als false interpretiert:
  • undefined, null (siehe auch Fallgrube 2)
  • Boolean: false
  • Number: -0, +0, NaN
  • String: ''
Alle anderen Werte gelten als true. Im Englischen hat sich der Begriff „falsy“ (in etwa: „fälschlich“, im Sinne von „bläulich“) für Werte etabliert, die äquivalent zu false sind und „truthy“ für wahr-äquivalente Werte. Den interaktiven Test, welcher Wert wie interpretiert wird, kann man über die Funktion Boolean machen, die ihr Argument in einen Wahrheitswert umwandelt:
    > Boolean(undefined)
    false
    > Boolean(0)
    false
    > Boolean(3)
    true
Überraschend ist z.B., dass wirklich nur der leere String falsy ist:
    > Boolean('')
    false
    > Boolean('false')
    true
    > Boolean('0')
    true

Implizite Umwandlung von Strings

In der Webentwicklung kommt es öfters mal vor, dass man als String erhält, was eigentlich eine Zahl oder ein Wahrheitswert ist. Zum Beispiel, wenn Benutzer diese Art von Daten in ein Formular eingeben. Vergisst man, diese Strings explizit umzuwandeln, dann überrascht einen JavaScript in zweierlei Hinsicht negativ: Erstens wird man nicht gewarnt. Zweitens wird automatisch umgewandelt, aber falsch. Die Addition (+) ist z.B. riskant, da sie Strings aneinanderhängt, sobald einer der Operanden ein String ist. Im folgenden denkt man, man addiert 1 zu 5. In Wahrheit fügt man aber die Zeichenketten '5' und '1' zusammen:
    > var x = '5';  // falsche Annahme: Zahl
    > x + 1
    '51'
Ausserdem gibt es ein paar Werte, die falsy sind, nach der Umwandlung zu String aber truthy werden. Das untersuchen wir mit Hilfe der folgenden Funktion genauer.
    function truthyOrFalsy(value) {
        return value ? 'truthy' : 'falsy';
    }
Beispiel: false.
    > truthyOrFalsy(false)
    'falsy'
    > String(false)
    'false'
    > truthyOrFalsy('false')
    'truthy'
Beispiel: undefined.
    > truthyOrFalsy(undefined)
    'falsy'
    > String(undefined)
    'undefined'
    > truthyOrFalsy('undefined')
    'truthy'

Implizite Umwandlung von Objekten

Eine implizite Umwandlung von Objekten findet nur statt, wenn eine Zahl oder eine Zeichenkette erwartet wird. Im ersten Fall findet die Umwandlung in drei Schritten statt:
  1. Rufe die Methode valueOf() auf. Ist das Ergebnis primitiv (kein Objekt), verwende es und wandle es in eine Zahl um.
  2. Rufe andernfalls die Methode toString() auf. Ist das Ergebnis primitiv, verwende es und wandle es in eine Zahl um.
  3. Wirf andernfalls einen TypeError.
Beispiel für Schritt 1:
    > 3 * { valueOf: function () { return 5 } }
    15
Beispiel für Schritt 3:
    > function returnObject() { return {} }
    > 3 * { valueOf: returnObject, toString: returnObject }
    TypeError: Cannot convert object to primitive value
Im zweiten Fall, wenn eine Zeichenkette erwartet wird, sind Schritt 1 und 2 vertauscht: zuerst wird toString() aufgerufen, dann valueOf().

Fallgrube: zwei „Nicht-Werte“ undefined und null

Die meisten Programmiersprachen haben nur einen Wert für „kein Wert“ bzw. „leer“. In Java ist das z.B. null. JavaScript hat unnötigerweise zwei dieser speziellen Werte: undefined und null.

undefined wird von der Sprache selbst zugewiesen. Variablen, die noch nicht initialisiert wurden, haben diesen Wert.

    > var foo;
    > foo
    undefined
undefined wird ebenso für nicht übergebene Parameter verwendet:
    > function id(x) { return x }
    > id()
    undefined

null wird wenn, dann von Programmierern verwendet, z.B. um anzugeben, dass ein Wert fehlt.

Check: hat eine Variable einen Wert?

Will man wissen, ob eine Variable v einen Wert hat, muss man also sowohl auf undefined als auch auf null prüfen. Glücklicherweise sind beide Werte truthy. Mit einer Überprüfung auf Truthiness per if schlägt man also zwei Fliegen mit einer Klappe:
    if (v) {
        // v hat einen Wert
    } else {
        // v hat keinen Wert
    }
Einziges Manko: auch false, -0, +0, NaN und '' werden als „kein Wert“ interpretiert. Wenn einem das nicht Recht ist, kann man diese kompakte Art der Überprüfung nicht verwenden. Sie ist aber trotzdem sehr beliebt, u.a. zur Parameterbehandlung. In Abschnitt 5 sind hierzu Beispiele zu sehen.

Fallgrube: die normale Gleicheit (==)

Eine Grundregel gleich vorweg: Die normalen Gleichheitsoperatoren == und != sind so problematisch, dass man immer die strengen Gleichheitsoperatoren === und !== verwenden sollte – ohne Ausnahme (auch wenn man manchmal gegenteiliges liest). Mit dieser einfachen Regel im Kopf können wir uns nun in Ruhe ansehen, was an == seltsam ist.

Der normale Gleichheitsoperator (==) hat viele Macken. Er ist zwar tolerant, aber es gelten nicht die üblichen Regeln für truthy und falsy:

    > 0 == false  // OK
    true
    > 1 == true  // OK
    true
    > 2 == true  // nicht OK
    false

    > '' == false  // OK
    true
    > '1' == true  // OK
    true
    > '2' == true  // nicht OK
    false
Ausserdem lassen sich Werte vergleichen, die man eigentlich nicht vergleichen kann:
    > '' == 0
    true
    > '123' == 123
    true
Details zu == können Sie hier nachlesen: 2ality.com/2011/06/javascript-equality.html

Bei der strengen Gleichheit (===) können Werte unterschiedlichen Typs nie gleich sein, weshalb keines der oben genannten Probleme auftritt.

Fallgrube: unbekannte Variablennamen erschaffen globale Variablen

Normalerweise legt JavaScript automatisch globale Variablen an, wenn man einen unbekannten Variablennamen verwendet:
    > function f() { foo = 123 }
    > f()
    > foo
    123
Im ECMAScript 5 Strict Mode wird dankenswerterweise gewarnt:
    > function f() { 'use strict'; foo = 123 }
    > f()
    ReferenceError: foo is not defined

Fallgrube: Parameterbehandlung

Die grundlegende Parameterbehandlung ist einfach, für fortgeschrittene Aufgaben wird allerdings Handarbeit benötigt. Es gibt zwei wichtige Grundregeln zur Parameterbehandlung.

Grundregel 1: Einer Funktion können beim Aufruf beliebig viele Argumente übergeben werden, egal wie viele Parameter in der Funktionsdefinition stehen. Jedem fehlenden Parameter wird der Wert undefined gegeben. Argumente, die zu viel sind, werden ignoriert. Lassen Sie uns beispielsweise von folgender Funktion ausgehen:

    function f(x, y) {
        console.log('x: '+x);
        console.log('y: '+y);
    }
Diese Funktion kann man mit beliebig vielen Argumenten aufrufen:
    > f()
    x: undefined
    y: undefined

    > f('a')
    x: a
    y: undefined

    > f('a', 'b')
    x: a
    y: b

    > f('a', 'b', 'c')
    x: a
    y: b

Grundregel 2: Alle übergebenen Parameter sind über die Array-ähnliche Variable arguments zugänglich. Mit folgender Funktion können wir uns ansehen, wie sie funktioniert.

    function f() {
        console.log('Anzahl: '+arguments.length);
        console.log('Werte: '+arguments);
    }
Die Funktion im Einsatz:
    > f()
    Anzahl: 0
    Werte: 

    > f('a')
    Anzahl: 1
    Werte: a

    > f('a', 'b')
    Anzahl: 2
    Werte: a,b
arguments ist immer vorhanden, egal wie viele Parameter explizit spezifiziert wurden. Es enthält immer alle Argumente.

Wurde ein Parameter übergeben?

Wenn ein Aufrufer einen Parameter nicht angibt, wird der aufgerufenen Funktion undefined übergeben. Dadurch, dass undefined falsy ist, kann man eine if-Anweisung verwenden, um zu überprüfen, ob ein Parameter existiert oder nicht:
    function hasParameter(param) {
        if (param) {
            return 'Ja';
        } else {
            return 'Nein';
        }
    }
Folglich bekommt man das selbe Ergebnis, wenn man einen Parameter weglässt und wenn man undefined übergibt:
    > hasParameter()
    'Nein'
    > hasParameter(undefined)
    'Nein'
Für alle truthy Werte funktioniert der Test ebenfalls bestens:
    > hasParameter([ 'a', 'b' ])
    'Ja'
    > hasParameter({ Name: 'Jane' })
    'Ja'
Allein bei anderen falsy Werten muss man aufpassen. So werden z.B. false und der leere String als fehlende Parameter gewertet:
    > hasParameter(false)
    'Nein'
    > hasParameter('')
    'Nein'
Dennoch hat sich dieses Muster bewährt. Man muss zwar ein klein wenig aufpassen, der Code ist aber angenehm kompakt und es ist egal, ob man undefined oder null verwendet.

Standardwerte für Parameter

Die folgende Funktion soll man mit 0 bis 2 Parametern aufrufen können. x und y sollten den Wert 0 bekommen, wenn sie nicht angegeben werden.
    function add(x, y) {
        if (!x) x = 0;
        if (!y) y = 0;
        return x + y;
    }
Interaktion:
    > add()
    0
    > add(5)
    5
    > add(2, 7)
    9
Manchmal sieht man auch eine Variante, die den “Oder”-Operator (||) verwendet. Diesen Operator schreibt man wie folgt.
    x || y
Das Ergebnis ist x, wenn x truthy ist, andernfalls y. Beispiele:
    > 'abc' || 'def'
    'abc'
    > '' || 'def'
    'def'
    > undefined || { foo: 123 }
    { foo: 123 }
    > { foo: 123 } || 'def'
    { foo: 123 }
Damit kann man Standardwerte für Parameter auch wie folgt zuweisen:
    function add(x, y) {
        x = x || 0;
        y = y || 0;
        return x + y;
    }

Eine variable Anzahl von Parametern

arguments kann man auch verwenden, um eine variable Anzahl von Parametern zuzulassen. Ein Beispiel ist die folgende Funktion format, die der klassischen C-Funktion sprintf nachempfunden ist:
    > format('Hallo %s! Sie haben %s Nachricht(en).', 'Jane', 5)
    'Hallo Jane! Sie haben 5 Nachricht(en).'
Das erste Argument ist ein Muster, in dem die zwei Zeichen '%s' Lücken kennzeichnen, in die die darauffolgenden Werte eingesetzt werden. Eine einfache Implementierung von format sieht wie folgt aus.
    function format(pattern) {
        for(var i=1; i < arguments.length; i++) {
            pattern = pattern.replace('%s', arguments[i]);
        }
        return pattern;
    }
Beachten Sie: In der Schleife wird der nullte Parameter, pattern, übersprungen und mit dem ersten Parameter danach begonnen.

Eine bestimmte Anzahl von Parametern erzwingen

Will man den Aufrufer zwingen, eine bestimmte Anzahl von Parametern zu verwenden, bleibt einem nur eine Überprüfung von arguments.length, zur Laufzeit:
    function add(x, y) {
        if (arguments.length !== 2) {
            throw new Error('Genau 2 Parameter benötigt');
        }
        return x + y;
    }

arguments ist kein Array

arguments ist kein Array, es ist nur Array-ähnlich: Man kann zwar mit arguments[i] auf den i-ten Parameter zugreifen und per arguments.length abfragen, wie viele Parameter es gibt. Aber Array-Methoden wie forEach und indexOf stehen nicht zur Verfügung. Weitere Details werden bei Fallgrube 8 erklärt.

Fallgrube: der Geltungsbereich von Variablen

Üblicherweise gelten Variablen in Programmiersprachen nur innerhalb des Blockes, in dem sie deklariert wurden. In JavaScript gelten sie in der gesamten umgebenden Funktion:
    function func(x) {
        console.log(tmp); // undefined
        console.log(xyz); // ReferenceError: xyz is not defined
        if (x < 0) {
            var tmp = 100 - x;
            ...
        }
    }
Tatsächlich findet bei einer Variablendeklaration „Hoisting“ (Anheben) statt: Die Deklaration wird an den Anfang der Funktion geschoben (eine initialisierende Zuweisung aber nicht). Sprich: die obenstehende Funktion sieht intern wie folgt aus.
    function func(x) {
        var tmp;
        console.log(tmp); // undefined
        console.log(xyz); // ReferenceError: xyz is not defined
        if (x < 0) {
            tmp = 100 - x;
            ...
        }
    }
Über einen Trick kann man in JavaScript aber den Geltungsbereich einer Variablen auf einen Block beschränken:
    function func(x) {
        console.log(tmp); // ReferenceError: tmp is not defined
        if (x < 0) {
            (function () {  // IIFE
                var tmp = 100 - x;
                ...
            }());
        }
    }
Hier wurde im Inneren der if-Anweisung eine Funktion definiert und gleich aufgerufen. Damit gilt tmp wirklich nur dort. Beachten Sie, dass die Klammern am Anfang und am Ende zwingend notwendig sind, da erst sie die Funktion zu einem Ausdruck machen. Leider können nur Ausdrücke sofort ausgeführt werden. Eine derart eingesetzte Funktion heißt „IIFE“ (ausgesprochen: „Iffi“), von englisch „Immediately Invoked Function Expression“ (sofort aufgerufener Funktionsausdruck).

Fallgrube: Closures und freie (externe) Variablen

Ein sehr mächtiges Feature von JavaScript sind Closures (deutsch: Abschlüsse): Wenn eine Funktion den Ort verlässt, wo sie erschaffen wurde, dann hat sie immer noch Zugriff auf die Variablen, die zu ihrer Geburt existierten. Beispielsweise:
    function incrementorFactory(start, step) {
        return function () {  // (*)
            start += step;
            return start;
        }
    }
Hier hat die innere Funktion (*) während ihrer gesamten Lebenszeit Zugriff auf die Variablen start und step. Es wird also nicht nur die Funktion zurückgegeben, sondern eine Kombination aus der Funktion und den Variablen start und step. Die Datenstruktur, in der die beiden Variablen gespeichert sind, heißt Environment (deutsch Umgebung). Ein Environment ist sehr ähnlich zu einem Objekt und wird in der Funktion abgelegt, was die Funktion zur Closure macht. Der Name erklärt sich daher, dass das Environment die Funktion abschließt: alle Variablen haben jetzt Werte, nicht nur die, die innerhalb der Funktion deklariert wurden. Im Einsatz sieht incrementorFactory wie folgt aus:
    > var inc = incrementorFactory(20, 2);
    > inc()
    22
    > inc()
    24

Die Fallgrube

Closures bekommen also nicht eine Momentaufnahme, sondern „lebende Variablen“ mit. Das führt im folgenden Code zu Problemen:
    var result = [];
    for (var i=0; i < 5; i++) {
        result.push(function () { return i });  // (*)
    }
    console.log(result[3]()); // 5 (nicht 3!)
Man erwartet vielleicht, dass jede Funktion, die an der Stelle (*) in das Array gestellt wird, den aktuellen Wert von i erhält. Stattdessen bricht die Verbindung zu dem „lebenden“ i nie ab. Und dessen Wert ist nach der Schleife 5. Eine mögliche Lösung des Problems ist, den aktuellen Wert der Variablen i per IIFE (siehe oben) zu kopieren:
    var result = [];
    for (var i=0; i < 5; i++) {
        (function (i2) {  // Momentaufnahme von i
            result.push(function () { return i2 });
        }(i));
    }
    console.log(result[3]()); // 3
Eine weitere Möglichkeit ist, forEach zu verwenden, dort werden in jedem Schleifendurchlauf neue Variablen erzeugt (aufgrund des Funktionsaufrufs):
    var result = [];
    [0,1,2,3,4].forEach(function (i) {
        result.push(function () { return i });
    });
    console.log(result[3]()); // 3

Fallgrube: Array-ähnliche Objekte

Manche Objekte in JavaScript sehen aus wie Arrays, sind aber keine. Man bezeichnet sie als Array-ähnlich. Array-ähnliche Objekte
  • haben: indizierten Zugriff auf Elemente und das Property length, das angibt, wie viele Elemente das Objekt enthält.
  • haben nicht: Array-Methoden wie push, forEach und indexOf.
Das bekannteste Array-ähnliche Objekt ist die spezielle Variable arguments. Man kann die Anzahl der Argumente ermitteln per
    arguments.length
Und man kann auf einzelne Argumente zugreifen, z.B. auf das erste Argument:
    arguments[0]
Array-Methoden muss man sich aber borgen. Das geht, weil die meisten dieser Methoden generisch sind.

Generische Methoden

Eine generische Array-Methode setzt nicht voraus, dass this ein Array ist, sie fordert nur ein Objekt, das indizierten Elementzugriff und length hat. Normalerweise ruft man eine Array-Methode m für ein Array a wie folgt auf:
    a.m(arg0, arg1, ...)
Alle Funktionen haben eine Methode call, mit der man diesen Aufruf auch anders ausführen kann:
    Array.prototype.m.call(a, arg0, arg1, ...)
Das erste Argument von call ist der Wert für this, den m erhält. In diesem Fall ist das a. Dadurch dass wir auf m direkt und nicht über a zugreifen haben wir nun die Möglichkeit, ein anderes this zu verwenden, z.B. arguments:
    Array.prototype.m.call(arguments, arg0, arg1, ...)
Nun zu einem konkreten Beispiel. Die folgende Funktion printArgs gibt alle Argumente aus, die sie erhält:
    function printArgs() {
        Array.prototype.forEach.call(arguments,
            function (arg, i) {
                console.log(i+'. '+arg);
            });
    }
Die Funktion im Einsatz:
    > printArgs()
    > printArgs('a')
    0. a
    > printArgs('a', 'b')
    0. a
    1. b
Will man hingegen arguments verändern, sollte man es als erstes in ein Array umwandeln. Das geht wie folgt:
    Array.prototype.slice.call(arguments);
Vergleiche: Von einem Array a erzeugt man eine Kopie per
    a.slice()

Fallgrube: Vererbung zwischen Konstruktoren

In JavaScript verwendet man Konstruktoren, um Typen zu implementieren. Sie sind Objektfabriken und entsprechen lose Klassen in anderen Sprachen.

Die zwei Prototypen

In JavaScript wird der Begriff „Prototyp“ leider doppelt verwendet.

Beziehung zwischen Objekten. Auf der einen Seite gibt es die Prototypbeziehung zwischen Objekten, durch die ein Objekt alle Propertys seines Prototyps erbt. Intern wird diese Beziehung hergestellt, indem ein Objekt über das interne Property [[Prototype]] auf sein Prototyp-Objekt verweist. Extern kann man dieses Property nicht sehen, aber man kann per Object.create() ein Objekt herstellen, das einen gegebenen Prototyp hat:

    > var prototyp = { foo: 'abc' };
    > var objekt = Object.create(prototyp);
    > objekt.foo
    'abc'
Und man kann per Object.getPrototypeOf() den Wert des Propertys auslesen:
    > Object.getPrototypeOf(objekt) === prototyp
    true

Property von Konstruktoren. Auf der anderen Seite gibt es das Property prototype, das Konstruktoren haben. Dieses hat als Wert ein Objekt, das zum Prototyp aller Instanzen des Konstruktors wird.

    > function Foo() {}
    > var f = new Foo();
    > Object.getPrototypeOf(f) === Foo.prototype
    true
Um Missverständnissen vorzubeugen, kann man das Objekt in prototype auch als Instanz-Prototyp bezeichnen.

Alleinstehende Konstruktoren

Einen alleinstehenden Konstruktor K zu implementieren ist einfach: Alle Instanzdaten werden in K dem Objekt in this hinzugefügt. Alle Instanz-übergreifenden Daten (vor allem die Methoden), werden in das Objekt in K.prototype gesteckt. Ein Beispiel:
    function Person(name) {
        this.name = name;
    }
    Person.prototype.describe = function () {
        return 'Person called ' + this.name;
    }
aPerson ist eine Instanz des Konstruktors Person. Zwischen den Objekten aPerson und Person.prototype besteht eine Prototypbeziehung.
Die Instanzdaten sind in diesem Fall this.name. Instanzübergreifend steht die Methode describe zur Verfügung. Ein Detail, das gleich noch wichtig wird, ist, dass jeder Instanz-Prototyp mit dem Property constructor auf den Konstruktor zurückverweisen sollte. Dadurch kann man zu jeder Instanz den Konstruktor finden, z.B. um neue Instanzen anzulegen. Standardmäßig ist bereits alles richtig konfiguriert:
    > function Foo() {}
    > Foo.prototype.constructor === Foo
    true

Sub-Konstruktoren

Will man Vererbung zwischen Konstruktoren einsetzen, wird es um einiges komplizierter. Lassen Sie uns beispielsweise Employee als Sub-Konstruktor von Person implementieren.
    function Employee(name, title) {
        Person.call(this, name);  // (1)
        this.title = title;
    }
    Employee.prototype = Object.create(Person.prototype);  // (2)
    Employee.prototype.constructor = Employee;  // (3)
    Employee.prototype.describe = function () {
        return Person.prototype.describe.call(this)  // (4)
               + ' (' + this.title + ')';
    }
Employee ist ein Sub-Konstruktor von Person. Beachten Sie, dass letzterer unverändert bleibt. Employee.prototype erbt durch die Prototypbeziehung alle Methoden von Person.prototype. Die Instanzdaten werden geerbt, indem Employee Person aufruft.
Es gibt folgende Dinge zu beachten:
  1. Der Sub-Konstruktor muss den Super-Konstruktor aufrufen, damit dieser seine Instanzdaten hinzufügen kann. Das geschieht per call-Methode, da der Super-Konstruktor das aktuelle this verwenden muss und kein eigenes Instanzobjekt erzeugen soll.
  2. Der Sub-Prototyp muss vom Super-Prototypen erben, da er auf diesem Weg die Super-Methoden erhält.
  3. Durch Schritt 2 haben wir das Objekt weggeworfen, in dem constructor richtig gesetzt ist. Hier müssen wir nachbessern.
  4. Aufrufe von überschriebenen Methoden (Super-Methoden) sind leider kompliziert. Wir greifen direkt auf die Super-Methode Person.prototype.describe zu und geben ihr per call() das aktuelle this mit.

Eine Hilfsfunktion für Konstruktor-Vererbung

Wenn man all dies nicht händisch machen will, kann man eine Hilfsfunktion programmieren:
    function inherits(SubC, SuperC) {
        var subProto = Object.create(SuperC.prototype);
        // Sichere `constructor` und ggf. schon vorhandene Methoden:
        copyOwnPropertiesFrom(subProto, SubC.prototype);
        SubC.prototype = subProto;
        SubC._super = SuperC.prototype;
    };
    function copyOwnPropertiesFrom(target, source) {
        Object.getOwnPropertyNames(source)
        .forEach(function(propName) {
            Object.defineProperty(target, propName,
                Object.getOwnPropertyDescriptor(source, propName));
        });
        return target;
    }
Die Funktion copyOwnPropertiesFrom kopiert „eigene“ (nicht geerbte) Propertys von source zu target und verwendet dazu Property-Descriptoren (Mehr Information: 2ality.com/2012/10/javascript-properties.html). Dank inherits lässt sich der Code für Employee eleganter schreiben:
    function Employee(name, title) {
        Employee._super.constructor.call(this, name);  // (*)
        this.title = title;
    }
    inherits(Employee, Person);
    Employee.prototype.describe = function () {
        return Employee._super.describe.call(this)
               + ' (' + this.title + ')';
    }
Angenehmerweise nehmen wir nicht mehr direkt auf den Super-Konstruktor Person bezug, sondern verweisen per Employee._super auf dessen Instanz-Prototypen. Als Folge davon wird leider der Aufruf des Super-Konstruktors an der Stelle (*) etwas länglich.

Lesen und Schreiben von Propertys

Sowohl das Lesen als auch das Schreiben von Propertys kann zu Überraschungen führen. In JavaScript unterscheidet man geerbte Propertys und „eigene“ Propertys eines Objektes obj. Erstere existieren in einem der Prototypen von obj, letztere in obj selbst. Mit Object.keys() kann man die eigenen Propertys eines Objektes ermitteln. Im folgenden Beispiel hat obj das eigene Property foo.
    > var obj = { foo: 123 };
    > Object.keys(obj)
    [ 'foo' ]
Geerbte Propertys hat es hingegen mehr, unter anderem toString und hasOwnProperty:
    > 'toString' in obj
    true
    > 'hasOwnProperty' in obj
    true

Lesen von unbekannten Propertys

Bei dem Lesezugriff
    obj.prop
passiert folgendes: Hat obj ein eigenes Property prop, so wird dessen Wert zurückgegeben. Hat obj ein geerbtes Property prop, so wird dessen Wert zurückgegeben. Andernfalls wird undefined zurückgegeben. Die Fallgrube hierbei ist, dass ein falsch geschriebener Propertyname keine Ausnahme (Exception) auslöst. Stattdessen bekommt man den Wert undefined zurück, der, wenn überhaupt, erst später zu einer Ausnahme führt.

Zuweisen zu unbekannten Propertys

Bei der Zuweisung
    obj.prop = ...
passiert folgendes: Hat obj ein eigenes Property prop, so wird dessen Wert geändert. Andernfalls wird ein neues eigenes Property prop zu obj hinzugefügt und mit einem Wert versehen. Dabei wird prop selbst dann hinzugefügt, wenn es dieses Property schon in einem der Prototypen von obj gibt. Beispiel: Gegeben sei das Objekt obj, dessen direkter Prototyp proto ist:
    var proto = { color: 'blue' };
    var obj = Object.create(proto);
Man kann color per obj lesen und obj hat keine eigenen Propertys:
    > obj.color
    'blue'
    > Object.keys(obj)
    []
Weist man aber color einen Wert zu, so wird ein eigenes Property angelegt:
    > obj.color = 'green';
    > Object.keys(obj)
    [ 'color' ]
proto wurde hierbei nicht verändert:
    > proto.color
    'blue'
Die Fallgrube ist, dass man bei Tippfehlern nicht gewarnt wird, sondern einfach ein neues Property angelegt wird.

Diese Art der Zuweisung schützt also Prototyp-Propertys davor, verändert zu werden. Dennoch wird davon abgeraten, Standardwerte dort abzulegen, denn wenn man in Objekte hineingreift, wirkt der Schutz nicht:

    > var proto = { loc: { x: 10, y: 10 } };
    > var obj = Object.create(proto);

    > obj.loc.x = 3;
    > proto.loc
    { x: 3, y: 10 }

Fallgrube: this in echten Funktionen

Funktionen spielen in JavaScript drei Rollen: echte Funktion, Konstruktor, Methode. Bedarf für this besteht nur bei den letzten beiden Rollen. Leider haben aber auch echte Funktionen ihr eigenes this, was zu einer Reihe von Problemen führt.

Fallgrube: this in echten Funktionen in einer Methode

Im folgenden Objekt wird in der Methode printFriends an der Stelle (*) eine echte Funktion eingesetzt.
    var jane = {
        name: 'Jane',
        friends: ['Tarzan', 'Cheeta'],
        printFriends: function () {
            this.friends.forEach(function (friend) {  // (*)
                console.log(this.name+' knows '+friend);  // (**)
            });
        }
    }
printFriends funktioniert nicht korrekt:
    > jane.printFriends()
    undefined knows Tarzan
    undefined knows Cheeta
Das liegt daran, dass die echte Funktion (*) this verwendet (**) und erwartet, dass es sich dabei um das this von printFriends handelt. Tatsächlich verweist this aber auf das „globale Objekt“ (window in Browsern). Das ist immer der Fall, wenn eine Funktion als echte Funktion und nicht als Methode aufgerufen wird:
    > (function () { return this }()) === window
    true
Da es die globalen Variable name nicht gibt, wird zweimal undefined ausgegeben.
Warnungen per Strict Mode
Wenn man den „strengeren“ Strict Mode von ECMAScript 5 anschaltet, dann hat this in echten Funktionen einen anderen Wert, nämlich undefined.
    > (function () { 'use strict'; return this }())
    undefined
Daher erhält man nun eine Warnung, wenn man this falsch verwendet. Man muss nur den Strict Mode anschalten, indem man eine Zeile am Anfang einfügt:
    'use strict';
    var jane = {
        ...
Die Warnung sieht so aus:
    > jane.printFriends()
    TypeError: Cannot read property 'name' of undefined
Lösungen
Es gibt zwei Lösungen. Die üblichere sieht wie folgt aus:
    printFriends: function () {
        var that = this;
        this.friends.forEach(function (friend) {  // (*)
            console.log(that.name+' knows '+friend);
        });
    }
Sprich: Man merkt sich das richtige this in der Variablen that, die nicht von der Funktion an der Stelle (*) überschattet wird. Eine andere Möglichkeit ist, die Methode bind zu verwenden, durch die eine neue Funktion erzeugt wird, die ein festes this hat.
    printFriends: function () {
        this.friends.forEach(function (friend) {
            console.log(this.name+' knows '+friend);
        }.bind(this));  // (*)
    }
An der Stelle (*) wird eine Funktion erzeugt, deren this immer gleich dem this-Wert von printFriends ist.

Fallgrube: this in extrahierten Methoden

Wenn man eine Methode aus einem Objekt entnimmt, wird es wieder zu einer echten Funktion. Damit reißt die Verbindung zum Objekt ab und die Methode funktioniert meist nicht mehr einwandfrei. Nehmen wir zum Beispiel das folgende Objekt counter:
    var counter = {
        count: 0,
        inc: function () {
            this.count++;
        }
    }
In JavaScript gibt es viele Funktionen und Methoden, die Callbacks (Rückruffunktionen) erwarten. In Browsern zum Beispiel setTimeout() und Event Handler. Wenn wir counter.inc als Callback übergeben wollen, bekommen wir Probleme. Diese lassen sich am besten demonstrieren, indem wir eine einfache Callback-aufrufende Funktion zu Hilfe nehmen:
    function callIt(callback) {
        callback();
    }
Wir verwenden nun callIt, um counter.count aufzurufen. Leider scheint der Aufruf keine Wirkung zu haben:
    > callIt(counter.inc)
    > counter.count
    0
Das Problem ist, dass counter.inc eine echte Funktion ist, wenn es in callIt aufgerufen wird. Folglich zeigt this wieder auf das globale Objekt und es wurde versucht, die globale Variable count um Eins zu erhöhen. Auch hier können wir bind() einsetzen:
    > callIt(counter.inc.bind(counter))
    > counter.count
    1
Nun wird callIt mit einer neuen Funktion aufgerufen, deren this den festen Wert counter hat. Damit klappt das Erhöhen von counter.count.

Fallgrube: this und falsch aufgerufene Konstruktoren

Vergisst man bei einem Aufruf eines Konstruktors versehentlich new, so wird er als echte Funktion aufgerufen. Also läuft man erneut Gefahr, globale Variablen zu erzeugen:
    function Point(x, y) {
        this.x = x;
        this.y = y;
    }
Im normalen Einsatz ruft man Point mit new auf:
    > var p = new Point(3, 5);
    > p.x
    3
    > p.y
    5
Vergisst man new, so erhält p den Wert undefined und globale Variablen x und y werden erzeugt:
    > var p = Point(3, 5);  // fehlt: new
    > p
    undefined
    > x
    3
    > y
    5
Auch hier empfiehlt es sich, den Strict Mode zu verwenden. Wir schalten ihn diesmal nur lokal, innerhalb von Point an.
    function Point(x, y) {
        'use strict';
        this.x = x;
        this.y = y;
    }
Nun wird man gewarnt, wenn man new vergisst:
    > var p = Point(3, 5);
    TypeError: Cannot set property 'x' of undefined

Fallgrube: die for-in-Schleife

Die for-in-Schleife kann verwendet werden, um über die Namen der Propertys eines Objektes zu iterieren:
    > var modelT = { name: 'Ford Model T', year: 1908 };
    > for (var propName in modelT) console.log(propName);
    name
    year
Diese Art der Schleife hat jedoch zwei große Mängel: Erstens wird bei Objekten über alle Propertynamen iteriert, auch über die Namen von geerbten Propertys. Zweitens wird auch bei Arrays über alle Propertynamen iteriert. Also weder über die Elemente (was nützlicher wäre), noch ausschließlich über Indizes. Die folgenden Unterabschnitte erklären genauer, was das bedeutet.

Iterieren über Objekte

Die for-in-Schleife berücksichtigt alle Propertys, also auch geerbte. [Anmerkung: nicht „aufzählbare“ (engl. enumerable) Propertys werden hingegen von for-in ignoriert. Aufzählbarkeit ist eine konfigurierbare Eigenschaft von Propertys. Details können Sie hier nachschlagen: 2ality.com/2012/10/javascript-properties.html ]

Um zu sehen, warum das problematisch sein kann, erstellen wir einen Konstruktor Car für Objekte wie das oben erwähnte modelT.

    function Car(name, year) {
        this.name = name;
        this.year = year;
    }
    Car.prototype.toString = function () {
        return this.name+' ('+this.year+')';
    };
Dieser Konstruktor wird wie folgt eingesetzt:
    > var modelT = new Car('Ford Model T', 1908);
    > modelT.toString()
    'Ford Model T (1908)'
Wenn wir nun über die Propertynamen iterieren, so sehen wir auch das geerbte Property toString.
    > for (var propName in modelT) console.log(propName)
    name
    year
    toString

Iterieren über Arrays

Wenn man über Arrays iteriert, will man normalerweise mit den Elementen arbeiten. Leider iteriert for-in über die Indizes und nicht nur über diese, sondern über alle Propertynamen.
    > var arr = [ 'a', 'b', 'c' ];
    > arr.foo = 'bar';

    > for (var i in arr) console.log(i)
    0
    1
    2
    foo

Alternativen zu for-in

Die generelle Empfehlung ist, for-in zu vermeiden. ECMAScript 5 bietet bessere Alternativen. Für Arrays kann man die forEach-Methode verwenden:
    arr.forEach(function (elem, index) {
        console.log(index+'. '+elem);
    });
Für Objekte kann man Object.keys() und forEach() kombinieren:
    Object.keys(obj).forEach(function (key) {
        console.log(key);
    });

Fazit und Ausblick

JavaScript hat einige Fallgruben. In diesem Artikel haben wir uns die 12 größten davon angesehen und wie man sie am besten umgeht. Dennoch ist JavaScript weniger komplex als viele gängige Programmiersprachen. Die Fallgruben muss man als Teil der Sprache lernen, das erhöht den Lernaufwand ein wenig, aber nicht viel.

ECMAScript 5 und ältere Browser

In diesem Artikel haben wir ECMAScript-5-Funktionen und Methoden wie Object.keys() und forEach() verwendet. Diese kann man in allen modernen Browsern verwenden. Sollten Sie Rücksicht auf ältere Browser nehmen müssen, haben Sie zwei Möglichkeiten:
  • Sie können die ECMAScript-5-Funktionalität per Bibliothek nachrüsten.
    github.com/kriskowal/es5-shim
  • Sie können Underscore.js verwenden. Diese Bibliothek enthält vieles der ECMAScript-5-Funktionalität. Kann ein Browser ECMAScript 5, so reicht Underscore die Aufrufe zur Standardbibliothek durch.
    underscorejs.org

ECMAScript 6

Hier nochmals die Liste der Fallgruben:
  1. Implizite Umwandlungen von Werten
  2. Zwei „Nicht-Werte“ undefined und null
  3. Die normale Gleicheit (==)
  4. Unbekannte Variablennamen erschaffen globale Variablen
  5. Parameterbehandlung
  6. Der Geltungsbereich von Variablen
  7. Closures und freie (externe) Variablen
  8. Array-ähnliche Objekte
  9. Vererbung zwischen Konstruktoren
  10. Lesen und Schreiben von Propertys
  11. this in echten Funktionen
  12. Die for-in-Schleife
ECMAScript 6 wird viele dieser Fallgruben eliminieren, durch folgende Features:
  • Mächtige Parameterbehandlung (Fallgrube 5 und teilweise 8, da man arguments nicht mehr benötigen wird).
  • let-Deklarationen für Variablen mit Block-Gültigkeitsbereich (Fallgrube 6).
  • Eine vielseitige for-of-Schleife (Fallgrube 12).
  • Wenn man let in einer der drei for-Schleifen (for, for-in, for-of) verwendet, so wird bei jedem Schleifendurchlauf eine neue Bindung der Schleifenvariable angelegt (Fallgrube 7).
  • Klassen, die – trotz ihres Namens – nur eine angenehmere Syntax für Konstruktoren sind (Fallgrube 9).
  • Das Schlüsselwort super, um Super-Methoden aufzurufen (teilweise Fallgrube 9).
  • Arrow Functions, echte Funktionen ohne eigenes this (teilweise Fallgrube 11).
Sprich: Fallgruben 1, 2, 3 und 10 werden längerfristig erhalten bleiben, die restlichen werden dank Strict Mode und ECMAScript 6 bald verschwinden.

Danksagungen

Dank an Brendan Eich für Feedback zu einer ersten Liste mit Fallgruben und für den Vorschlag, Fallgrube 8 aufzunehmen. Dank an Claude Pache für den Vorschlag, die Umwandlung von Strings zu erwähnen. Dank an Tobias Schneider für diverse Verbesserungsvorschläge.

No comments: