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.
Die folgenden Abschnitte behandeln je eine Fallgrube. Am Ende wird ein Ausblick auf ECMAScript 6 gegeben, das viele der Fallgruben eliminiert.
> '5' - '2' 3 > '5' * '2' 10Der 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.
> 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
> 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'
> 3 * { valueOf: function () { return 5 } } 15Beispiel für Schritt 3:
> function returnObject() { return {} } > 3 * { valueOf: returnObject, toString: returnObject } TypeError: Cannot convert object to primitive valueIm zweiten Fall, wenn eine Zeichenkette erwartet wird, sind Schritt 1 und 2 vertauscht: zuerst wird toString() aufgerufen, dann valueOf().
undefined wird von der Sprache selbst zugewiesen. Variablen, die noch nicht initialisiert wurden, haben diesen Wert.
> var foo; > foo undefinedundefined 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.
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.
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 falseAusserdem lassen sich Werte vergleichen, die man eigentlich nicht vergleichen kann:
> '' == 0 true > '123' == 123 trueDetails 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.
> function f() { foo = 123 } > f() > foo 123Im ECMAScript 5 Strict Mode wird dankenswerterweise gewarnt:
> function f() { 'use strict'; foo = 123 } > f() ReferenceError: foo is not defined
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,barguments ist immer vorhanden, egal wie viele Parameter explizit spezifiziert wurden. Es enthält immer alle Argumente.
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.
function add(x, y) { if (!x) x = 0; if (!y) y = 0; return x + y; }Interaktion:
> add() 0 > add(5) 5 > add(2, 7) 9Manchmal sieht man auch eine Variante, die den “Oder”-Operator (||) verwendet. Diesen Operator schreibt man wie folgt.
x || yDas 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; }
> 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.
function add(x, y) { if (arguments.length !== 2) { throw new Error('Genau 2 Parameter benötigt'); } return x + y; }
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).
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
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]()); // 3Eine 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
arguments.lengthUnd 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.
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. bWill 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()
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 trueUm Missverständnissen vorzubeugen, kann man das Objekt in prototype auch als Instanz-Prototyp bezeichnen.
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. |
> function Foo() {} > Foo.prototype.constructor === Foo true
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. |
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.
> 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
obj.proppassiert 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.
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 }
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 CheetaDas 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 trueDa es die globalen Variable name nicht gibt, wird zweimal undefined ausgegeben.
> (function () { 'use strict'; return this }()) undefinedDaher 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
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.
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 0Das 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 1Nun wird callIt mit einer neuen Funktion aufgerufen, deren this den festen Wert counter hat. Damit klappt das Erhöhen von counter.count.
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 5Vergisst 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 5Auch 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
> var modelT = { name: 'Ford Model T', year: 1908 }; > for (var propName in modelT) console.log(propName); name yearDiese 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.
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
> var arr = [ 'a', 'b', 'c' ]; > arr.foo = 'bar'; > for (var i in arr) console.log(i) 0 1 2 foo
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); });