This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Ihr Kontakt zum Verlag und Lektorat: [email protected] Bibliografische Information Der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.ddb.de abrufbar.
Korrektorat: mediaService, Siegen Satz: mediaService, Siegen Titelgrafik: Melanie Hahn Umschlaggestaltung: Caroline Butz Belichtung, Druck & Bindung: M.P. Media-Print Informationstechnologie GmbH, Paderborn Alle Rechte, auch für Übersetzungen, sind vorbehalten. Reproduktion jeglicher Art (Fotokopie, Nachdruck, Mikrofilm, Erfassung auf elektronischen Datenträgern oder andere Verfahren) nur mit schriftlicher Genehmigung des Verlags. Jegliche Haftung für die Richtigkeit des gesamten Werks kann, trotz sorgfältiger Prüfung durch Autor und Verlag, nicht übernommen werden. Die im Buch genannten Produkte, Warenzeichen und Firmennamen sind in der Regel durch deren Inhaber geschützt.
Vorwort Mit Web 2.0 hat sich die Herangehensweise bei der Entwicklung von Webseiten fundamental geändert. Während früher noch mit Photoshop ein Layout erarbeitet wurde (es gibt sogar Leute, die so etwas mit Excel getan haben), wird heute meist ein XHTML-Gerüst – einem Groblayout folgend – durch CSS und Javascript immer weiter verfeinert. Diese inkrementelle Vorgehensweise bei der Webentwicklung ist so etwas wie das Markenzeichen von Web 2.0 geworden: Irgendwie ist alles immer beta, man abeitet noch daran, man sucht noch nach den besten Features. Alles ist im Fluss, wird ständig verändert. Web-Applikationen sind flüchtige Gebilde geworden, die morgen schon ganz anders aussehen können als heute. Dieser Wesenszug war auch immer schon in der Natur von Javascript verankert, einer Sprache, die minimale Erwartungen an die Entwicklungsumgebung stellt und deren größte Stärke Flexibilität ist – auch bei der eigenen Neuerfindung. Syntaktische Eleganz liegt im Wesen der Programmiersprache, die für viele Entwickler ein rotes Tuch darstellt. Ist Javascript komplizierter als andere Progammiersprachen? Komplizierter als C++, Perl oder Prolog? Mitnichten! Diese häufig kolportierte Fehleinschätzung liegt meiner Ansicht nach in der Differenz zwischen Leistungsfähigkeit und typischem Einsatzgebiet begründet. Wer eine einfache Formularprüfung machen möchte, schlägt sich ungern mit dem Konzept Prototypen-basierter Objektorientierung herum. Die Zeiten haben sich geändert: Moderne Webapplikationen abstrahieren die gewöhnungsbedürftigen Konzepte von Javascript und lassen eine Eleganz und Leichtigkeit durchscheinen, die ganz im Sinne von Web 2.0 ist. Die Autoren dieses Buches wussten das wahrscheinlich schon immer – und können deshalb mit ihren Beiträgen der Leichtigkeit ein solides Fundament geben: Jörg Schaible erläutert Unit-Tests für Javascript, Tobias Struckmeier blickt unter die Haube des populären JSON-Formats, Chuck Easttom klärt Missverständnisse im Umgang mit Datum- und Zeitangaben in Javascript auf, und Arne Blankerts beschäftigt sich in seinem Beitrag über AJAX mit dem software-technischen Kern des Web 2.0-Phänomens, dem XMLHttpRequest-Objekt, auf das an einigen Stellen in diesem Buch Bezug genommen wird.
Exploring JavaScript
9
Vorwort
In seiner einfachsten Form lässt sich ein Wrapper wie folgt formulieren: function getXmlHttp() { if ( window.XMLHttpRequest ) { return new XMLHttpRequest(); } else { return new ActiveXObject( "Microsoft.XMLHTTP" ); } };
Listing V.1 Die Datei xhttp.js
Ich selbst lote schließlich in meinem Beitrag die Möglichkeiten objektorientierten Programmierens in Javascript aus. Der vorliegende Band bringt Ihnen damit die syntaktische Flexibilität von Javascript näher und zeigt Ihnen, dass viele der häufig beschworenen Nachteile von Javascript eigentlich Ausdruck großer Leistungsfähigkeit sind. Potsdam, im Februar 2007 Markus Nix
10
1
Fortgeschrittene Techniken der Objektorientierung
Von Markus Nix Douglas Crockford, Lichtgestalt der finsteren JavaScript-Welt und Erfinder des JSON-Formats, muss frustriert gewesen sein, als er 2001 einen Artikel verfasste mit dem Titel „JavaScript: The World's Most Misunderstood Programming Language“ [1]. Auch wer kein leidenschaftlicher JavaScript-Entwickler ist, dürfte bei der Lektüre nicht ungerührt bleiben, denn in gewisser Weise ist der Beitrag beißende Polemik und zärtliche Liebeserklärung zugleich. Ein Artikel über eine Sprache, die flexibel und mächtig ist wie kaum eine andere, vielleicht aber aufgrund eben diesen Umstands in der Vergangenheit häufig nur auf Unverständnis und Ratlosigkeit gestoßen ist. Gerade die ersten Bücher zu JavaScript behandeln das Thema Objektorientierung stiefmütterlich – wenn überhaupt. Auch in Zeiten von Ajax und Web 2.0 und der damit einhergehenden Renaissance der Programmiersprache JavaScript gibt es eine gewaltige Grauzone im Entwicklerlager, die flankiert wird von enthusiastischen Verfechtern auf der einen und dilettantischen Formularprüfern auf der anderen Seite. In der Mitte aber herrscht bange Ratlosigkeit. Die Zerrissenheit wurde JavaScript schon in die Wiege gelegt: Aufgerieben zwischen willfährigen Marketing-Spezialisten kennen wir die Sprache als Mocha, LiveScript, JScript, EcmaScript und ActionScript. JavaScript ist als Programmiersprache die lingua franca des Webs, fast jeder PC auf dieser Welt beeinhaltet mindestens einen Interpreter. JavaScript ist so dynamisch, objektorientiert und universell, dass eine moderne JavaScriptBibliothek wie Prototype [2] kurzerhand den Ruby-Kernel inkl. der StandardKlassen nach JavaScript portiert hat – elegante Iteratoren-Syntax und zahlreiche Patterns inklusive. Halten wir es fürs Protokoll mal fest: JavaScript hat soviel mit Java zu tun wie Reinhold Messner mit Tiefseetauchen. Dennoch: Der Vergleich wird immer und überall gezogen. Nehmen wir also im Rahmen dieses Artikels die Herausforderung an und klopfen JavaScript auf jene Features ab, die man an einer Hochsprache wie Java rühmt, z.B. Interfaces oder Zugriffsschutz. Geht nicht? Geht doch! Denn JavaScript ist so flexibel, dass man sich diese Features selbst hinzuprogrammieren kann. Und einige mehr dazu, die Java nicht kennt, z.B. die automatische Erstellung von Getter- und Setter-Methoden, wie sie RubyProgrammierer schätzen. Im Wesentlichen sind drei Features von JavaScript verantwortlich für ein Höchstmaß an Eleganz und Flexibilität: die Erweiterbarkeit Exploring JavaScript
11
1 – Fortgeschrittene Techniken der Objektorientierung
von Instanzen zur Laufzeit auf der Grundlage einer Prototypen-basierten Objektorientierung, die Möglichkeit, eine Funktion als Argument an eine andere Funktion zu übergeben und nicht zuletzt Closures. Die meisten Beispiele in diesem Beitrag verwenden diese Features auf die eine oder andere Art. Unverständlich ist mir, warum sich die meisten Entwickler so schwer tun mit JavaScript, wo die Anleihen an andere Sprachen Legion sind: C, Lisp oder Scheme. JavaScript kennt auch Lambda-Ausdrücke, doch dazu später mehr. Fatal auch, dass im Grunde JavaScript noch immer Nachteile zugeschrieben werden, die längst schon Geschichte sind: Die Ermangelung von Ausnahmebehandlungen, Vererbung oder innere Funktionen gehören hierzu. Zu Fehlern im Sprachentwurf (welche Sprache hat sie nicht) kamen fehlerhafte Browser, schlechte Bücher, unverständliche Spezifikationen. Am Ende war JavaScript der Fußabtreter unter den Programmiersprachen. Jeder hatte eine Meinung, wenige jedoch hatten genügend Ahnung, um die Ausdruckskraft von JavaScript hinreichend zu nutzen. Für die Programmiersprache JavaScript ist Web 2.0 ein Segen, weil die damit verbundene Hinwendung zum Browser zur Folge hat, dass man besser entwickeln möchte, eleganter, performanter. Das führt zwangsläufig zur Umsetzung der Prinzipien objektorientierter Programmierung, die im Falle von JavaScript nicht klassenbasiert, sondern Prototypen-basiert ist. JavaScript hat kein Klassenkonzept wie Java, erlaubt aber dennoch Konstruktoren, Methoden, Eigenschaften. Diese objektorientierten Features werden JavaScript häufig abgesprochen – mit Verweis auf mangelnde Fähigkeiten der Kapselung oder der Vererbung. Beides ist möglich – mit einer überraschenden Anzahl an Implementierungsmöglichkeiten. Beginnen wir mit einem Feature, das – typisch für JavaScript! – unterschätzt wird, jedoch die Grundlage darstellt für zahlreiche fortgeschrittene Möglichkeiten objektorientierter Programmierung:
1.1 Zugriffsschutz Listing 1.1 zeigt, wie sich „private“ Methoden in JavaScript durch das Weglassen eines Methoden-Pointers realisieren lassen. Nur die Methoden, die durch das Schlüsselwort this einen Methoden-Pointer erhalten, sind öffentlich sichtbar. function MyClass( parameterA, parameterB ) { propertyR = "propertyR is only readable."; // private
Listing 1.1 Private Methoden durch das Weglassen eines Methoden-Pointers
Listing 1.1 Private Methoden durch das Weglassen eines Methoden-Pointers (Forts.)
Wir sollten diesem Aspekt etwas mehr Aufmerksamkeit zukommen lassen, nicht zuletzt, weil in der Literatur allzu oft kolportiert wird, dass JavaScript über keinen Zugriffsschutz verfügt. Das ist grundlegend falsch. Die Grundfesten der Objektorientierung in Sprachen wie C++ oder Java bilden die Konzepte der Klasse und des Objekts. Eine Klasse ist ein strukturierter komplexer Typ, der als eine Art Vorlage für die zugehörigen Objekte, auch Instanzen genannt, dient. Letztere werden nach dem vorgegebenen Schema der Klasse erzeugt. Weiterhin definieren Klassen lediglich die Datenstruktur und die entsprechenden Methoden, auf ihnen selbst kann jedoch nicht operiert werden. Das ist nur mit den konkreten Ausprägungen, eben den Objekten, möglich (sehen wir einmal von statischen Funktionen ab). JavaScript kennt im Gegensatz zur klassenbasierten Vererbung die Vererbung basierend auf Prototypen. Dieser Ansatz differenziert nicht zwischen Objekten und Klassen: Statt Klassenschablonen gibt es prototypische Objekte. Jedes Objekt kann zur Laufzeit um beliebige Methoden und Attribute erweitert werden. Dieser konzeptionelle Unterschied hat in der Vergangenheit zahlreiche Autoren dazu verleitet, JavaScript ein Geheimnisprinzip (vgl. Zugriffsmodifizierern public, protected, private in Java) abzusprechen. Zu Unrecht, da es durchaus möglich ist, in JavaScript nicht-öffentliche Methoden zu erzeugen. Schauen wir JavaScript zunächst mal unter die Haube. In JavaScript ist alles ein Objekt: Arrays sind Objekte, Funktionen sind Objekte, Objekte sind Objekte. Oder besser gesagt: Schlüssel-Wert-Paare. Die Schlüssel sind Strings, die Werte, was immer JavaScript an Datentypen hergibt. Lehnen wir uns etwas aus dem Fenster und nennen einen Wert, der eine Funktion darstellt, eine Methode. Mit Hilfe des Schlüsselwortes this greifen wir dann auf Werte einer Instanz zurück. Zunächst einmal sind alle Mitglieder eines Objekts 14
Zugriffsschutz
öffentlich, egal ob wir sie im Konstruktor definieren oder sie an die prototypeEigenschaft hängen: function MyClass( val ) { this.member = val; }; var my = new MyClass( 'a' );
Die Eigenschaft my.member enthält a. In der Praxis definiert man häufig Eigenschaften im Konstruktor, während Methoden an die prototype-Eigenschaft angehängt werden: MyClass.prototype.getMember() { return this.member; };
Private Eigenschaften sind nun jene, die im Konstruktor verwendet werden – und zwar einzig unter Verwendung des Schlüsselworts var. function My Class( val ) { this.member = val; var value = 3 var self = this; };
Die Klasse verfügt somit über drei (!) private Eigenschaften: val, value und self. Sie sind an das Objekt gebunden, aber sie sind nicht sichtbar von außen. Sie sind noch nicht mal sichtbar für die öffentlichen Methoden des jeweiligen Objekts. Sie sind einzig sichtbar für die privaten Methoden, die realisiert werden als innere Funktionen des Konstruktors: function MyClass( val ) { function say() { alert( value ); }; this.member = val;
Listing 1.2 Innere Funktionen des Konstruktors als private Methoden
Exploring JavaScript
15
1 – Fortgeschrittene Techniken der Objektorientierung
var value = 3 var self = this; };
Listing 1.2 Innere Funktionen des Konstruktors als private Methoden (Forts.)
Die Konvention empfiehlt, immer eine Variable self oder auch that einzuführen, die die Instanz auch für private Methoden verfügbar macht: function MyClass( val ) { function say() { alert( self.member ); }; this.member = val; var value = 3 var self = this; say(); }; var test = new MyClass( 5 );
Listing 1.3 Instanz auch für private Methoden verfügbar machen
Private Methoden können nicht durch öffentliche Methoden aufgerufen werden. Das ist natürlich nur bedingt erstrebenswert. Um das Konzept der privaten Methoden nützlicher zu machen, führen wir privilegierte Methoden ein. Eine privilegierte Methode kann auf private Eigenschaften und Funktionen zugreifen, ist selbst aber sichtbar für öffentliche Methoden von außerhalb. Privilegierte Methoden erzeugt man unter Verwendung von this im Konstruktor. function MyClass( val ) { function say() { alert( self.member ); }; this.member = val;
Listing 1.4 Privilegierte Methoden
16
Zugriffsschutz var value = 3 var self = this; this.sayHello = function() { say(); }; }; var test = new MyClass( 5 ); test.sayHello();
Listing 1.4 Privilegierte Methoden (Forts.) sayHello ist eine privilegierte Methode, auf die von außen zugegriffen werden kann, die selbst aber wiederum Zugriff auf die private innere Funktion say hat. Sie werden zugeben müssen, dass diese Vorgehensweise einen ähnlichen Zugriffsschutz gewährleistet, wie wir es z.B. von Java, PHP 5, Python oder Ruby kennen. Möglich ist diese Funktionsweise in JavaScript durch Closures. Innere Funktionen haben in JavaScript immer Zugriff auf die Variablen in der umgebenden Funktion. Das ist eine mächtige Funktion von JavaScript, die in der Literatur häufig unterschlagen wird. Eine der wenigen guten Einführungen finden Sie unter http://jibbering.com/faq/faq_notes/closures.html. Bedenken Sie, dass private und privilegierte Eigenschaften und Funktionen eines Objekts nur bei der Instanziierung des Objekts erstellt werden können, öffentliche hingegen zu jeder Zeit. Übrigens können wir auch statische Member privat machen. Die Vorgehensweise baut dabei auf dem oben genannten Prinzip auf: Der Aufruf des Konstruktors definiert ein Closure, das alle Parameter, lokale Variablen und Funktionen mit dem Objekt assoziiert. Innere Funktionen, die an Eigenschaften der Instanz gebunden werden (z.B. mit this.myMethod = function() {...};), sind dann „privilegiert“, weil sie direkten Zugriff auf die privaten Eigenschaften des Objekts haben. Nur durch diese privilegierten Methoden kann Zugriff genommen werden auf private Eigenschaften, nicht jedoch durch öffentliche Methoden. Wenn Sie mit anderen objektorientierten Sprachen vertraut sind, wird Ihnen der von Douglas Crockford geprägte Begriff „privileged“ sicher komisch vorkommen, aber er beschreibt auf recht anschauliche Art die Sonderrolle der inneren Funktionen des Konstruktors. Java kennt neben Modifiern wie private, protected und public noch andere, z.B. static. Ein statisches Member ist Mitglied der Klasse, nicht eines Objekts. Von diesem Member existiert zur Laufzeit nur eine
Exploring JavaScript
17
1 – Fortgeschrittene Techniken der Objektorientierung
Kopie. Üblicherweise werden statische Member in JavaScript als Eigenschaften des Konstruktors definiert: function MyClass() { }; MyClass.counter = 0;
Derlei statische Member sind natürlich öffentlich, es ist jedoch auch möglich, Crockfords Ideen zu nutzen, um statische private Eigenschaften zu deklarieren. Wieder bedienen wir uns eines Closures: var MyObject = ( function() { // private static class member var counter = 0; // private static method function incInstanceCount() { return counter++; }; // class constructor function constructorFunc( id ) { this.id = id; var self = this; // call private static class method // and assign the returned index to // a private instance member var index = incInstanceCount(); // privileged instance method this.getIndex = function() { return index; }; }; // privileged static class method // (a property of the constructor) constructorFunc.getInstanceCount = function() {
Listing 1.5 Private statische Eigenschaften
18
Zugriffsschutz return counter; }; // public instance method priviliged at the // class level constructorFunc.prototype.pubInstMethod = function() { }; // return the constructor return constructorFunc; } )(); // public static member MyObject.pubStatic = "anything" // public instance member MyObject.prototype.pubInstVar = 8;
Die gleichzeitige Definition und der Aufruf in Form eines Closures wird hier verwendet, um den Konstruktor der Klasse zurückzugeben. Dieser Konstruktor enthält nun auch private statische Eigenschaften sowie privilegierte statische Methoden – eben als Eigenschaften des Konstruktors. Das ist im Grunde die natürliche Weiterentwicklung der Vorgehensweise von Crockford – nur auf Klassenebene. In JavaScript haben innere Funktionen direkten Zugriff auf Parameter und lokale Variablen in der Funktion, in der sie enthalten sind. Im obigen Beispiel kann Code z.B. im Konstruktor die Funktion incInstanceCount() aufrufen. Öffentliche Instanzmethoden (also Eigenschaften von prototype) und statische Methoden (also Eigenschaften des Konstruktors, der von der inneren Funktion zurückgegeben wird) haben keinen Zugriff auf die privaten statischen Eigenschaften einer Klasse. Private statische Member funktionieren, weil alle Instanzen einer Klasse den gleichen Konstruktor haben. So können sie auch ein Closure teilen, welches den Konstruktor definiert und zurückgibt. Eingedenk dieser Tatsache gehen wir nun einen wesentlichen Schritt voran. Wenn Crockfords Idee bei Instanzen und Klassen funktioniert, warum nicht auch bei Gruppen von Klassen (packages in der Terminologie klassenbasierter Sprachen)? Obwohl der Ausdruck Packages sicher etwas zu hoch gegriffen ist... Dennoch: Wir können ungewöhnliche Effekte erzielen, wenn wir uns der Möglichkeiten verschachtelter Closures klar werden. Die Vorgehensweise ist recht ungewöhnlich, Exploring JavaScript
19
1 – Fortgeschrittene Techniken der Objektorientierung
hat aber bereits ihre Fans gefunden, z.B. die Entwickler von TrimPath (http:// www.trimpath.com). Bauen wir uns eine (anonyme) Closure zur Klassengruppierung. Beachten Sie dabei die Funktion privateToClassGroup(), die eine UtilityFunktion für alle Klassen dieser Gruppe enthalten könnte: var global = this; ( function() { var classGroupMember = 3; function privateToClassGroup(){ }; global.MyObject1 = function() { var privteStaticMember = 4; function privateStaticMethod() { }; function constructorFunc( id ) { }; return constructorFunc; }(); global.MyObject2 = function() { function constructorFunc( id ) { }; return constructorFunc; }(); global.MyObject3 = function() { function constructorFunc( id ) { }; return constructorFunc; }(); } )();
Listing 1.6 Verschachtelte Closures
20
Zugriffsschutz
Alle Instanzen einer Klasse teilen sich einen Konstruktor. Auch teilen sich alle Instanzen einer Klasse ein prototype-Objekt. Könnte demnach auch ein Closure assoziiert mit einem prototype-Objekt als Repository privater statischer Member dienen? Versuchen wir es: function MyClass() { }; MyClass.prototype = ( function() { // private static class member var privateStaticProp = "whatever"; // private static method function privateStaticMethod = function() { }; return ( { // These functions objects are shared by // all instances that use this prototype // and they have access to the private static // members within the closure that returns // this object publicInstanceMethod: function() { }, setSomething: function( s ) { privateStaticProp = s; } } ); } )(); // public instance member MyObject.prototype.pubInstVar = 8;
Listing 1.7 Closure als Repository privater statischer Methoden
Funktioniert! Und ist besonders dann empfehlenswert, wenn private Instanzvariablen nicht benötigt werden und es auch keine inneren Funktionen des Konstruktors gibt, die auf die privaten statischen Eigenschaften der Klasse zugreifen wollen. Wie auch immer, Sie kennen nun zwei Möglichkeiten, private statische Member zu definieren. Exploring JavaScript
21
1 – Fortgeschrittene Techniken der Objektorientierung
1.2 Vererbung Aufgrund der Prototypen-basierten Objektorientierung ist Vererbung in JavaScript etwas anders gelöst als in bekannten objektorientierten Sprachen wie z.B. Java. Damit einher gehen gewisse Fallstricke. Nehmen wir uns einmal ein klassisches Beispiel vor: function Animal( name ){ this.name = name; this.offspring = []; }; Animal.prototype.haveABaby = function() { var newBaby = new Animal( "Baby " + this.name ); this.offspring.push( newBaby ); return newBaby; }; Animal.prototype.toString = function() { return '[Animal "' + this.name + '"]'; };
function Dog( name ) { this.name = name; }; // Here's where the inheritance occurs Dog.prototype = new Animal(); // Otherwise instances of Dog would have a constructor of Animal Dog.prototype.constructor = Dog; Dog.prototype.toString = function() { return '[Dog "' + this.name + '"]'; };
var someAnimal = new Animal( 'Sumo' );
Listing 1.8 Einfaches Beispiel für Vererbung
22
Vererbung var myPet = new Dog( 'Spinky Bilane' ); // results in 'someAnimal is [Animal "Sumo"]' alert( 'someAnimal is ' + someAnimal ); // results in 'myPet is [Dog "Spinky Bilane"]' alert( 'myPet is ' + myPet ); // calls a method inherited from Animal myPet.haveABaby(); // shows that the dog has one baby now alert( myPet.offspring.length ); // results in '[Animal "Baby Spinky Bilane"]' alert( myPet.offspring[0] );
Listing 1.8 Einfaches Beispiel für Vererbung (Forts.)
Schauen Sie sich noch einmal die letzte Zeile an. Das Baby eines Hundes sollte doch auch ein Hund sein, nicht wahr? Die haveABaby-Methode hat ihre Arbeit korrekt verrichtet, weil sie explizit eine neue Instanz von Animal erzeugt hat. Wir könnten nun natürlich eine haveABaby-Methode innerhalb der Dog-Klasse implementieren, elegant wäre es allerdings nicht. Viel besser wäre es, wenn die Methode der Basisklasse gleich ein Objekt vom richtigen Typ erzeugen würde, z.B. mit Hilfe dieser Methode: Animal.prototype.haveABaby = function() { var newBaby = new this.constructor( "Baby " + this.name ); this.offspring.push( newBaby ); return newBaby; } // ... // same as before: calls the method inherited from Animal myPet.haveABaby(); // now results in '[Dog "Spinky Bilane"]' alert( myPet.offspring[0] );
Exploring JavaScript
23
1 – Fortgeschrittene Techniken der Objektorientierung
Jede Instanz in JavaScript kennt eine Eigenschaft constructor, die auf den eigenen Konstruktor verweist. Mit diesem Wissen haben wir nun eine Methode, die immer den korrekten Konstruktor aufruft. Was nun, wenn man aber explizit den Konstruktor der Elternklasse aufrufen möchte? Derzeit kennt JavaScript noch keine Eigenschaft super, die auf die Elternklasse verweist, stattdessen können wir aber die call-Methode des Function-Objektes verwenden, die uns diese Funktionalität bietet. Dog.prototype.haveABaby = function() { Animal.prototype.haveABaby.call( this ); alert( "I am a dog" ); };
Wenn Sie dem Methodenaufruf Parameter hinzufügen wollen, so können Sie diese nach dem this platzieren. Die oben gezeigte Konstruktion ist ziemlich gewöhnungsbedürftig. Da wir als JavaScript-Entwickler natürlich für die Flexibilität der Sprache Reklame machen wollen, schlage ich einen alternativen Weg vor: // ... Dog.prototype = new Animal(); Dog.prototype.constructor = Dog; Dog.prototype.superclass = Animal.prototype; // ... Dog.prototype.haveABaby = function(){ var theDoggy = this.superclass.haveABaby.call( this ); alert( "I am a dog" ); return theDoggy; };
Listing 1.9 Verweis auf Superklasse
Hier speichern wir die Elternklasse in der Eigenschaft superclass, damit wir auf sie jederzeit zugreifen können. Das ist ein bisschen eleganter, aber immer noch viel zu kompliziert. Bauen wir uns ein Helferlein: /** * Helper method for easy handling of inheritance. * * @access public
Listing 1.10 Helper-Funktion für das Ableiten von Klassen
24
Vererbung */ Function.prototype.extend = function( parentConstructor, className ) { var f = new Function(); if ( parentConstructor ) { f.prototype = parentConstructor.prototype; proto = this.prototype = new f; proto.superclass = parentConstructor; } else { proto = this.prototype; proto.superclass = null; } proto.constructor = this; proto._prototype = this.prototype; if ( className ) { proto.classname = className; } return proto; };
Listing 1.10 Helper-Funktion für das Ableiten von Klassen (Forts.)
Diese Funktion hilft uns dabei, das Ableiten von Klassen künftig einfacher zu lösen. Über die unbedingt notwendige Funktionalität heraus speichert die Funktion auch den Namen der Klasse als String, um auf Reflection basierende Features zu erleichtern. Damit sieht Vererbung für uns künftig so aus: function BaseClass() { // Some code here }; _pt = BaseClass.extend( null, "BaseClass" );
function SubClass() { BaseClass.call( this ); };
Exploring JavaScript
25
1 – Fortgeschrittene Techniken der Objektorientierung _pt = SubClass.extend( BaseClass, "SubClass" ); _pt.someMethod() { // Some code here };
Bedeutend einfacher! Und kürzer, denn das [Klasse].prototype können wir uns in Zukunft sparen. Wie in der JavaScript-Welt üblich, reicht bereits ein kleines Code-Snippet, um die Gemüter zu erhitzen. Was könnte man gegen diese einfache Erweiterung des nativen Function-Objekts einwenden? Na, zum Beispiel, dass es überhaupt eine unschöne Sache ist, native Objekte zu erweitern. Das ist Geschmackssache – und es ist ganz nach meinem Geschmack, allerdings mit einer Ausnahme: Erweiterungen der prototype-Eigenschaft von Object führen zu unerwünschten Seiteneffekten, weil dadurch die Schlüssel-Wert-Zuordnung aufgebrochen wird. Das folgende kurze Beispiel zeigt das ganze Dilemma: var my_obj = { 'cars': ['Audi', 'BMW', 'Volkswagen'], 'foo': 'bar' }; Object.prototype.dump = function() { for ( o in this ) { alert( o + ':' + this[o] ); } }; my_obj.dump();
Listing 1.11 Probleme bei der Erweiterung von Object.prototype
Wir erzeugen eine Variable my_obj, die ein Objekt mit mehreren Schlüssel-WertZuordnungen enthält. Anschließend erweitern wir die prototype-Eigenschaft von Object um eine triviale Dump-Methode, die wir anschließend aufrufen, damit sie durch unser zuvor angelegtes Objekt iteriert und die einzelnen Schlüssel-WertZuordnungen anzeigt. Das Problem: Die Methode dump ist nun auch ein Mitglied des Objektes my_obj, was natürlich auf keinen Fall wünschenswert ist. Viele Autoren haben große Anstrengungen unternommen, dieses Verhalten zu umgehen, indem sie z.B. neue Methoden einem speziellen Stack hinzugefügt haben, der bei
26
Vererbung
jeder Verarbeitung eines Objekts befragt wurde. Ein guter Rat: Unterlassen Sie es einfach, Object.prototype zu erweitern, Sie ersparen sich eine Menge Probleme. Lassen Sie uns aber noch einmal das Hundebaby-Beispiel anschauen. Wir haben nämlich ein Problem erzeugt, das sich nicht gleich erschließt. Erschaffen wir zunächst einmal ein zweites Hundebaby: var myPet2 = new Dog( 'Pinsel' ); // results in 'myPet2 is [Dog "Pinsel"]' alert( 'myPet2 is ' + myPet2 ); myPet2.haveABaby(); // results in '2' alert( myPet2.offspring.length );
Das Objekt myPet2 hat zwei Babies gespeichert, obwohl es nur eins haben sollte! Warum? Weil Dog.prototype = new Animal(); eine einzelne Instanz von Animal in die prototype-Chain von Dog eingebracht hat. Jede Instanz von Dog verändert die gleiche Eigenschaft offspring einer einzigen Instanz von Animal. Die Eigenschaften der Instanz der Elternklasse sind prototype-Eigenschaften geworden und werden somit von allen Instanzen verwendet. Es gibt mehrere Möglichkeiten, dieses Verhalten zu umgehen. Die trivialste Möglichkeit ist die „Maskierung“: Dabei definieren wir einfach alle Eigenschaften erneut im Konstruktor der abgeleiteten Klasse, beispielsweise so: function Dog( name ) { this.name = name; this.offspring = []; };
Das ist natürlich kein ernst zu nehmender Vorschlag, weil diese Vorgehensweise dem Wesen der Ableitung zuwiderläuft. Wenn wir zur Implementierung der Klasse Dog die interne Datenstruktur von Animal wissen müssen, dann müssen wir uns vorwerfen lassen, das Prinzip objektorientierter Programmierung nicht verstanden zu haben, indem wir Kapselung verhindern. Hinzu kommt, dass im obigen Beispiel der Wert name nicht an den Konstruktor der Klasse Animal weitergereicht wird. Wenden wir uns also einer eleganteren Lösung zu und formulieren unser Beispiel neu: Exploring JavaScript
27
1 – Fortgeschrittene Techniken der Objektorientierung function Animal( name ){ this.name = name; this.offspring = []; }; Animal.prototype.haveABaby = function() { var newBaby = new Animal( "Baby " + this.name ); this.offspring.push( newBaby ); return newBaby; }; Animal.prototype.toString = function() { return '[Animal "' + this.name + '"]'; }; function Dog( name ) { Animal.call( this, name ); }; // Here's where the inheritance occurs Dog.prototype = new Animal(); // Otherwise instances of Dog would have a constructor of Animal Dog.prototype.constructor = Dog; Dog.prototype.toString = function() { return '[Dog "' + this.name + '"]'; }; var someAnimal = new Animal( 'Sumo' ); var myPet = new Dog( 'Spinky Bilane' ); // results in 'someAnimal is [Animal "Sumo"]' alert( 'someAnimal is ' + someAnimal ); // results in 'myPet is [Dog "Spinky Bilane"]' alert( 'myPet is ' + myPet ); // calls a method inherited from Animal
Listing 1.12 Ableitung ohne Seiteneffekte
28
Virtuelle Klassen myPet.haveABaby(); // shows that the dog has one baby now alert( myPet.offspring.length ); // results in '[Animal "Baby Spinky Bilane"]' alert( myPet.offspring[0] ); var myPet2 = new Dog( 'Pinsel' ); // results in 'myPet2 is [Dog "Pinsel"]' alert( 'myPet2 is ' + myPet2 ); myPet2.haveABaby(); // results in '1' alert( myPet2.offspring.length );
Listing 1.12 Ableitung ohne Seiteneffekte (Forts.)
Der Konstruktor der Klasse Animal wird aufgerufen. Im Kontext der Dog-Klasse (this) werden alle Elemente der Animal-Klasse in der Klasse Dog angelegt. Durch die Verwendung der Methode Function.call stellen wir zudem die Datenkapselung sicher. Vererbung lässt sich – Lob der syntaktischen Flexibilität von JavaScript! – aber auch anders realisieren. Douglas Crockford hat hierzu drei SugarMethoden geschrieben, die JavaScript ein wenig versüßen und z.B. parasitäre Vererbung ermöglichen [3].
1.3 Virtuelle Klassen Einige objektorientierte Sprachen kennen das Konzept virtueller Klassen, also Klassen, die nicht selbst instanziiert werden können, von denen man jedoch ableiten kann. Das lässt sich in JavaScript sehr einfach implementieren, indem wir die virtuelle Klasse als Objekt anlegen und nicht als Funktion. Wenn die Klasse keine Funktion ist, kann sie auch nicht als Konstruktor verwendet werden: LivingThing = { beBorn : function() { this.alive = true; }
Listing 1.13 Beispiel für eine virtuelle Klasse
Exploring JavaScript
29
1 – Fortgeschrittene Techniken der Objektorientierung }; function Animal( name ) { this.name = name; this.offspring = []; }; Animal.prototype = LivingThing; // Note: not 'LivingThing.prototype' Animal.prototype.superclass = LivingThing; Animal.prototype.haveABaby = function() { this.parent.beBorn.call( this ); var newBaby = new this.constructor( "Baby " + this.name ); this.offspring.push( newBaby ); return newBaby; }; Animal.prototype.toString = function() { return '[Animal "' + this.name + '"]'; }; // results in 'someAnimal is [Animal "Sumo"]' alert( 'someAnimal is ' + new Animal( 'Sumo' ) ); // error! new LivingThing();
Listing 1.13 Beispiel für eine virtuelle Klasse (Forts.)
1.4 Interfaces In der objektorientierten Programmierung vereinbaren Schnittstellen (engl. Interface) gemeinsame Signaturen von Klassen. Das heißt, eine Schnittstelle vereinbart die Signatur einer Klasse, die diese Schnittstelle implementiert. Das Implementieren einer Schnittstelle stellt eine Art Vererbung dar. Die Schnittstelle gibt an, welche Methoden vorhanden sind, bzw. vorhanden sein müssen. Schnittstellen repräsentieren eine Garantie bezüglich der in einer Klasse vorhandenen Methoden. Sie geben an, dass alle Objekte, die diese Schnittstelle besitzen, gleich behandelt werden können. Gleich vorweg: Nehmen Sie die fol30
Interfaces
gende Möglichkeit, Interfaces in JavaScript zu verwenden, nicht allzu ernst. Da es sich hierbei nicht um ein Sprachfeature handelt, ist die Verwendung von der Disziplin des Entwicklers abhängig – was dem Wesen eines Vertrages, den ein Interface ja darstellt, eigentlich zuwiderläuft. Bohren wir ein weiteres Mal das Function-Objekt auf. /** * Ensures that a function fulfills an interface. * * Since with ECMA 262 (3rd edition) interfaces * are not supported yet, this function will * simulate the functionality. The arguments for * the function are all classes that the current * class will implement. The function checks whether * the current class fulfills the interface of the * given classes or not. * * @throws Error * @access public */ Function.prototype.fulfills = function() { var I; for ( var i = 0; i < arguments.length; ++i ) { I = arguments[i]; if ( typeof I != "function" || !I.prototype ) { throw new Error( "Not an interface." ); } if ( !this.prototype ) { throw new Error( "Current instance is " + "not a function definition." ); } for ( var f in I.prototype ) { // don't take properties into consideration // which were added in Function.extend if ( f.toString() == "classname" || f.toString() == "superclass" ) {
Listing 1.14 Heler-Funktion zur Implementierung von Pseudo-Interfaces
Exploring JavaScript
31
1 – Fortgeschrittene Techniken der Objektorientierung continue; } if ( typeof I.prototype[f] != "function" ) { throw new Error( f.toString() + " is not a method in Interface " + I.toString() ); } if ( typeof this.prototype[f] != "function" && typeof this[f] != "function" ) { if ( typeof this.prototype[f] == "undefined" && typeof this[f] == "undefined" ) { throw new Error( f.toString() + " is not defined" ); } else { throw new Error( f.toString() + " is not a function" ); } } } } };
Listing 1.14 Heler-Funktion zur Implementierung von Pseudo-Interfaces (Forts.)
Dazu schreiben wir uns ein kleines Beispiel, das die Verwendung von – naja – Interfaces in JavaScript illustriert: function MyInterface() { this.constructor.fulfills( MyInterface ); }; MyInterface.prototype.requiredMethod = function() {}; function MyClass() { MyInterface.call( this ); }; var cls = new MyClass();
Listing 1.15 Pseudo-Interface
32
Getter- und Setter-Methoden MyInterface stellt in diesem Beispiel das Interface dar, das eine Methode requiredMethod enthält, die es in Klassen, die dieses Interface implementieren,
auszuformulieren gilt. Falls dies nicht der Fall ist, erfolgt eine Fehlermeldung. Sieht ganz nach einem echten Interface aus, finden Sie nicht? Bitte beachten Sie hierzu auch den Beitrag von Jörg Schaible über Unit-Tests in JavaScript.
1.5 Getter- und Setter-Methoden Sind wir mal ehrlich: Entwickler objektorientierter Programme verrichten einen großen Teil ihrer Zeit mit der immer gleichen Aufgabe, nämlich dem Implementieren von Getter- und Setter-Methoden (auch Accessor- und Mutator-Methoden genannt). Das sieht dann z.B. so aus: function Rectangle() { this._width = 100; this._height = 100; }; _pt = Rectangle.extend( null, "Rectangle" ); _pt.setWidth = function( width ) { this._width = width; }; _pt.getWidth = function() { return this._width; }; _pt.setHeight = function( height ) { this._height = height; }; _pt.getHeight = function() { return this._height; };
Listing 1.16 Einfache Klasse mit Getter- und Setter-Methoden
Langweilig? Langweilig! Und fehleranfällig! Bauen wir uns wieder ein kleines Helferlein:
Exploring JavaScript
33
1 – Fortgeschrittene Techniken der Objektorientierung Function.READ = 1; Function.WRITE = 2; Function.READ_WRITE = 3; /** * @access public */ Function.prototype.addProperty = function( sName, nRdWr, v ) { var p = this.prototype; nRdWr = nRdWr || Function.READ_WRITE; var capitalized = sName.charAt( 0 ).toUpperCase() + sName.substr( 1 ); if ( nRdWr & Function.READ ) { p["get" + capitalized] = function() { return this["_" + sName]; }; } if ( nRdWr & Function.WRITE ) { p["set" + capitalized] = function( v ) { this["_" + sName] = v; }; } if ( v ) { p["_" + sName] = v; } };
Listing 1.17 Helper-Funktion zur automatischen Generierung von Getter- und Setter-Methoden
34
Namensräume var rect = new Rectangle(); alert( rect.getHeight() );
Listing 1.17 Helper-Funktion zur automatischen Generierung von Getter- und Setter-Methoden (Forts.)
Eleganter, nicht wahr? Mit Hilfe der addProperty-Methode haben wir eine einfache Möglichkeit, Getter-, Setter- oder beide Methoden dynamisch zu erzeugen, so wie es aus Ruby bekannt ist, Default-Wert inklusive.
1.6 Namensräume Mit Namensräumen kann ein Entwickler große Programmpakete mit vielen definierten Namen schreiben, ohne sich Gedanken machen zu müssen, ob die neu eingeführten Namen in Konflikt zu anderen Namen stehen. Im Gegensatz zu der Situation ohne Namensräume wird hier nicht der ganze Name neu eingeführt, sondern nur ein Teil des Namens, nämlich der des Namensraumes. Ein Namensraum ist ein deklaratorischer Bereich, der einen zusätzlichen Bezeichner an jeden Namen anheftet, der darin deklariert wurde. Dieser zusätzliche Bezeichner macht es weniger wahrscheinlich, dass ein Namenskonflikt auftritt mit Namen, die anderswo im Programm deklariert wurden. Es ist möglich, den gleichen Namen in unterschiedlichen Namensräumen ohne Konflikt zu verwenden, auch wenn der gleiche Namen in der gleichen Übersetzungseinheit vorkommt. Solange sie in unterschiedlichen Namensräumen erscheinen, ist jeder Name eindeutig aufgrund des zugefügten Namensraumbezeichners. Namensräume in JavaScript 1.x sind nicht mit Namensräumen zu vergleichen, wie es sie z.B. in C++ gibt. Wie so häufig handelt es sich hierbei eher um eine programmiertechnische Konvention, beruhend auf der einfachen Erweiterbarkeit von Objektstrukturen. Bekannte Projekte wie die Yahoo UI-Bibliothek (YUI), Qooxdoo oder Prototype nutzen diese Konvention, um Namenskonflikte zu vermeiden und Code sauber zu organisieren. So findet sich die Drag & DropKlasse der Yahoo UI-Bibliothek etwa hier: var myDDobj = new YAHOO.util.DD( "myDiv" );
Der Kalender hier: var myCal = new YAHOO.widget.Calendar( "calEl", "container" );
Ein solcher Namensraum lässt sich ganz einfach aufbauen: Exploring JavaScript
35
1 – Fortgeschrittene Techniken der Objektorientierung if ( typeof MyNamespace == "undefined" ) { var MyNamespace = {}; } MyNamespace.SomeClass = function() { };
Dies ist deutlich eleganter als Namensungetüme wie Base_Security_Passwd_ Generator, finden Sie nicht? Sollten Sie in Ihrem Projekt von zahlreichen anderen Bibliotheken Gebrauch machen, sind Sie gut beraten, Ihre Skripte in einem eigenen Namensraum anzusiedeln, um Namensüberschneidungen von vornherein auszuschließen. Bitte beachten Sie im obigen Beispiel die explizite Prüfung auf die Existenz von MyNamespace. Im Gegensatz zu C# darf ein Namespace nur ein einziges Mal definiert werden. Wird diese Anweisung ein weiteres Mal ausgeführt, so werden alle definierten Member gelöscht. Es gibt sogar Hardliner, die mit ihrem Code den globalen Namensraum überhaupt nicht „verschmutzen“ (namespace pollution). Das lässt sich ganz einfach mit Hilfe eines Closures bewerkstelligen: ( function() { function MyClass() { } MyClass.prototype.sayHello = function() { alert( 'hello' ); }; // example code var test = new MyClass(); test.sayHello(); })(); alert( MyClass ); // -> undefined
1.7 Mehrfachvererbung In der Objektorientierung ist Vererbung eine Methode, neue Klassen unter Verwendung von bestehenden aufzubauen. Zu unterscheiden ist dabei Schnittstel-
36
Mehrfachvererbung
lenvererbung und Klassenvererbung. Bei der Schnittstellenvererbung „erbt“ eine abgeleitete Klasse die Signaturen von Methoden, muss die Methoden aber selbst implementieren. Bei der Klassenvererbung erbt die abgeleitete Klasse auch die Implementierung von einer oder mehreren Basisklassen. JavaScript bietet keine Klassenvererbung „out of the box“. Wir können dies aber simulieren (wenn Sie nicht allzu streng mit dem Beispiel sind): /** * Emulation of multiple inheritance for JavaScript. * * @param mixed arg Either an object or array * of objects * @param Boolean bInheritPrototype Whether to use prototype * members as well * @access public */ Function.prototype.inherits = function( arg, bInheritPrototype ) { if ( !arg ) { return; } if ( typeof arg != 'object' || arg.constructor != Array ) { arg = [arg]; } function getMembers( obj ) { var result = [], member; for ( member in obj ) { result[member] = obj[member]; } return result; }; var member, members, i; for ( i = 0; i < arg.length; i++ ) { // static members if ( arg[i].constructor ) {
Listing 1.18 Helper-Funktion zur Implementierung von Mehrfachvererbung
Exploring JavaScript
37
1 – Fortgeschrittene Techniken der Objektorientierung members = getMembers( arg[i] ); for ( member in members ) { this[member] = members[member]; } } // prototype members if ( bInheritPrototype && arg[i].prototype ) { members = getMembers( arg[i].prototype ); for ( member in members ) { this.prototype[member] = members[member]; } } } };
Listing 1.18 Helper-Funktion zur Implementierung von Mehrfachvererbung (Forts.)
Das folgende Beispiel zeigt eine Klasse Test, die Methoden und Eigenschaften der Klassen Car und Bus erbt. Car = function() { Car.counter++; }; Car.counter = 0; Car.prototype.stop = function() { alert( 'Stop' ); }; Car.prototype.drive = function() { alert( 'Drive' ); }; Bus = function() { Bus.counter++; }; Bus.counter = 0; Bus.crash = function() { alert( 'Crash' ); }; Bus.prototype.stopAtSchool = function() { alert( 'Pick up' ); };
Listing 1.19 Beispiel für Mehrfachvererbung
38
Überladen von Funktionen Test = function() { }; Test.inherits( [Car, Bus], true ); var ts = new Test(); ts.drive(); ts.stop(); ts.stopAtSchool(); Test.crash();
Listing 1.19 Beispiel für Mehrfachvererbung (Forts.)
1.8 Überladen von Funktionen Überladen bezeichnet in der Welt der Programmiersprachen die Erstellung von zwei oder mehr Funktionen mit demselben Namen. Welche Funktion aufgerufen wird, wird anhand der deklarierten Datentypen der Parameter entschieden. Eine ähnliche Funktionalität lässt sich auch in JavaScript erreichen. Erweitern wir dazu einfach den Sprachkern: /** * Allows functions to be overloaded (different versions of the * same function are called based on the arguments types). * * @access public * @static */ Function.overload = function() { var f = function( args ) { var i, l, h = ""; for ( i = -1, l = ( args = [].slice.call( arguments ) ).length; ++i < l; h += args[i].constructor ); if ( !( h = f._methods[h] ) ) { var x, j, k, m = -1; for ( i in f._methods ) {
Listing 1.20 Helper zur Funktionsüberladung (Forts.)
Die Funktion speichert verschiedene Signaturen ab in Abhängigkeit der Anzahl der Argumente sowie deren Konstruktoren. JavaScript ist nicht typlos, es ist dynamisch typisiert. Wir können also zur Laufzeit den Typ einer Variable ermitteln: alert( [].constructor == Array ); // -> true
Zurück zu unserem Beispiel. Wir erzeugen nun eine Funktion ol, die sich je nach Anzahl und Art der übergebenen Argumente anders verhält: ol = new Function.overload;
Listing 1.21 Beispiel für Funktionsüberladung
40
Design Patterns // one parameter which is a Number ol.overload( function( x ) { document.write( "NUMBER " ); }, Number ); // one parameter which is a String ol.overload( function( x ) { document.write( "STRING " ); }, String ); // two parameters, a Function and a Number ol.overload( function( x, y ) { document.write( "FUNCTION, NUMBER " ); }, Function, Number ); // two parameters, a Number and a String ol.overload( function( x, y ) { document.write("NUMBER, STRING " ); }, Number, String ); // tests ol( function() {}, 123 ); ol( 123 ); ol( "ABC" ); ol( 123, "ABC" ); ol( {} ); // remove function with Number parameter ol.unoverload( Number ); ol( {} );
Listing 1.21 Beispiel für Funktionsüberladung (Forts.)
1.9 Design Patterns Je mächtiger die Objektorientierung einer Sprache wird, umso wichtiger und notwendiger wird die Planung objektorientierter Softwareentwicklung. Design Patterns ebnen hier einen Weg, um wiederkehrende Entwurfsprobleme bei Softwareentwicklungsprozessen zu unterbinden und Lösungen in Form von bewährten Mustern (Patterns) bereitzustellen, um somit die Problemsituation zu erken-
Exploring JavaScript
41
1 – Fortgeschrittene Techniken der Objektorientierung
nen und so effizient wie möglich zu lösen. Die Intention und der Grundgedanke zur Verwendung von objektorientierter Software besteht in der Wiederverwendbarkeit (Code Reuse), um auch bei zukünftigen Anforderungen und Problemen zu bestehen. Design Patterns repräsentieren Programmteilstrukturen, die einfache und elegante Lösungen für spezifische Probleme des Softwareentwurfs beschreiben. Entwurfsmuster sind Beschreibungen von Lösungen für Software-Design-Probleme. Pattern-Beschreibungen müssen in einer festgelegten Form erfolgen, damit man die Patterns miteinander vergleichen und in ein Schema einordnen kann. Für diesen Zweck haben in den letzten Jahren viele Autoren Kataloge entwickelt, die auf die Anforderungen von Design Patterns für die Softwareentwicklung abgestimmt sind. Die Struktur des bekanntesten Katalogs wurde von Erich Gamma und seinen Kollegen Richard Helm, Ralph Johnson und John Vlissides (Gang of Four, kurz GoF) entwickelt. Die meisten GoF-Patterns sind objektbasiert, d.h., sie beziehen sich auf Objekte und ihre Beziehung zueinander. Erich Gamma und seine Mitautoren teilen Design Patterns nach zwei Klassifikationskriterien ein. Dies sind der Gültigkeitsbereich der Pattern und ihre Aufgabe. Nach dem Gültigkeitsbereich werden Patterns für Klassen (klassenbasiertes Muster) und Objekte (objektbasiertes Muster) unterschieden, da ausschließlich objektorientierte Patterns beschrieben werden. Nach der Aufgabe werden Patterns in die Kategorien „Creational“ (Erzeugungsmuster), „Structural“ (Strukturmuster) und „Behavioral“ (Verhaltensmuster) eingeteilt. Erzeugungsmuster beschäftigen sich mit der Erzeugung von Objekten, Strukturmuster beschreiben die statische Zusammensetzung von Objekten und Klassen, Verhaltensmuster charakterisieren das dynamische Verhalten von Objekten und Klassen. Die Gang of Four hat insgesamt 23 Patterns beschrieben, von denen wir hier einige Umsetzungen betrachten wollen.
1.9.1
Factory
Die Fabrikmethode ist den klassenbasierten Erzeugungsmustern zugeordnet. Sie ist ein effizienter Mechanismus zur Kapselung der Objekterzeugung – mit der Option, Unterklassen entscheiden zu lassen, von welcher Klasse das Objekt ist. Es handelt sich um ein häufig verwendetes Muster, da die Objekterzeugung zu den Standardaufgaben in der objektorientierten Programmierung gehört. Listing 1.22 zeigt, wie sich mit Hilfe der BrowserAbstractionFactory-Klasse Objektinstanzen erzeugen lassen, die auf den verwendeten Browser abgestimmt sind. Ältere DHTML-Bibiliotheken schleppen noch den Ballast für alle Browser mit
42
Design Patterns
sich herum. Es ist auch denkbar, die factory-Methode als statische Methode an das Browser-Objekt anzuhängen und mit Browser eine Default-Implementierung zu schaffen. // Base class for our Browser classes function Browser() { }; Browser.prototype.getBrowser = function() { return this.browser_type; }; // file BrowserMoz.js function BrowserMoz() { Browser.call( this ); this.browser_type = "Mozilla"; }; _pt = BrowserMoz.extend( Browser, "BrowserMoz" ); // file BrowserOpera.js function BrowserOpera() { Browser.call( this ); this.browser_type = "Opera"; }; _pt = BrowserOpera.extend( Browser, "BrowserMoz" ); // file BrowserIE.js function BrowserIE() { Browser.call( this ); this.browser_type = "Internet Explorer"; }; _pt = BrowserIE.extend( Browser, "BrowserIE" ); function BrowserAbstractionFactory() {
Listing 1.22 Einfaches Beispiel für Factory-Pattern
Exploring JavaScript
43
1 – Fortgeschrittene Techniken der Objektorientierung this.className = null; var isSupported = true; var var var var
Listing 1.22 Einfaches Beispiel für Factory-Pattern (Forts.)
Das „Nachladen“ der benötigten Klassen mittels document.write() ist nicht sehr elegant, aber ein gangbarer Weg. Natürlich ließe sich hier das XmlHttp-Objekt nutzen, um einen Loader-Mechanismus zu implementieren, es würde allerdings den Rahmen dieses Beitrages sprengen.
44
Design Patterns
1.9.2
Singleton
Das Singleton-Pattern kommt immer dann zum Einsatz, wenn von einer Klasse nur jeweils eine Instanz zur selben Zeit existieren darf. Listing 1.23 zeigt, wie sich das Singleton-Pattern in JavaScript realisieren lässt. function SingletonExample() { if ( SingletonExample._singleton ) { return SingletonExample._singleton; } SingletonExample._singleton = this; }; _pt = SingletonExample.extend( null, "SingletonExample" ); _pt.setValue = function( value ) { this.value = value; }; _pt.getValue = function() { return this.value; }; var singleton_obj = new SingletonExample(); singleton_obj.setValue( 5 ); // returns 5 alert( singleton_obj.getValue() ); var singleton_obj2 = new SingletonExample(); alert( singleton_obj2.getValue() ); // returns 5 as well
Listing 1.23 Singleton-Pattern
1.9.3
Proxy
Das Entwurfsmuster Proxy (proxy = Stellvertreter) liefert ein schönes Beispiel für die Kategorie der objektbasierten Strukturmuster. Hinter diesem Entwurfsmuster verbirgt sich eine einfache Idee: Eine Stellvertreter-Klasse verbirgt eine nachgeladene Klasse. Damit kann der Zugriff auf die nachgelagerte Klasse kontrolliert werden. Dies ist z. B. sinnvoll, wenn auf die nachgelagerte Klasse nur beschränkter Zugriff gewährt werden soll. Die Arbeit verrichtet die nachgelagerte Klasse,
Exploring JavaScript
45
1 – Fortgeschrittene Techniken der Objektorientierung
die Schnittstelle wird durch die Proxy-Klasse definiert. Das folgende Listing zeigt die generische Umsetzung dieses Konzepts in einfachster Form. function Implementation() { this.a = function() { return "a"; } this.b = function() { return "b"; } this.c = function() { return "c"; } }; function Proxy() { this.impl = new Implementation(); this.get = function( which ) { if ( this.impl[which] ) { return this.impl[which](); } else { return null; } } }; var proxy_obj = new Proxy(); alert( proxy_obj.get( "a" ) ); // returns "a" alert( proxy_obj.get( "z" ) ); // returns null
1.9.4
Template Method
Das Entwurfsmuster Template Method gehört zur Kategorie der objektbasierten Verhaltensmuster und beschreibt eine grundlegende Technik zur Wiederverwendung von Code. Mit dieser Technik wird das Gerüst eines Algorithmus in einer Operation aufgebaut. So ist es möglich, bestimmte Schritte des Algorithmus jederzeit auf unterer Ebene zu ändern (durch Überschreiben der Methoden in den Unterklassen), ohne die Struktur des Algorithmus anzutasten. Es wird also von 46
Design Patterns
oben eine bestimmte Struktur vorgegeben, die in den Unterklassen beliebig angepasst wird. Charakteristisch für Template Method ist, dass in der Basisklasse eine Methode definiert wird, die wiederum Methoden der Basisklasse aufruft, welche in der Unterklasse überschrieben werden. Manchmal verfügen die beschriebenen Methoden über ein Default-Verhalten (also schon eine Implementierung in der Basisklasse), das trotzdem auf unterer Ebene durch Überschreiben entsprechend modifiziert werden kann. Die Methode templateMethod delegiert die Aufgabe demzufolge an die abstrakten Methoden der Basisklasse, die ihrerseits in der Unterklasse überschreiben werden. Listing 1.24 zeigt die JavaScript-Version dieses Patterns: function Template() { }; _pt = Template.extend( null, "Template" ); _pt.templateMethod = function() { this.method1(); this.method2(); }; _pt.method1 = function() { throw new Error( "Not implemented" ); }; _pt.method2 = function() { throw new Error( "Not implemented" ); }; function Application() { Template.call( this ); }; _pt = Application.extend( Template, "Application" ); _pt.method1 = function() { alert( "Method 1" ); };
Listing 1.24 Template-Pattern
Exploring JavaScript
47
1 – Fortgeschrittene Techniken der Objektorientierung _pt.method2 = function() { alert( "Method 2" ); }; var app_obj = new Application(); app_obj.templateMethod();
Listing 1.24 Template-Pattern (Forts.)
1.9.5
Iterator
Objekte, die als Behälter für andere Objekte auftreten, wie z.B. Array oder Object, sollten in der Regel Methoden bereitstellen, die ein einfaches Iterieren über dieses Element erlauben. Listing 1.25 zeigt einen einfachen Array-Iterator: function ArrayIterator( arr ) { this.array = ( arr != null )? arr : []; this.arrayCount = 0; }; _pt = ArrayIterator.extend( null, "ArrayIterator" ); _pt.reset = function() { this.arrayCount = 0; }; _pt.hasMore = function() { if ( this.arrayCount == this.array.length ) { return false; } return true; }; _pt.next = function() { return this.array[this.arrayCount++]; }; _pt.count = function() { return this.array.length; };
Listing 1.25 Einfacher Array-Iterator
48
Design Patterns
var it = new ArrayIterator( new Array( 1, 2, 4, 8, 16, 32, 64 ) ); it.reset(); while ( it.hasMore() ) { alert( it.next() ); }
Listing 1.25 Einfacher Array-Iterator (Forts.)
Das obige Beispiel für die Implementierung des Iterator-Patterns ist zugegebenermaßen ziemlich trivial. Wir verdanken es in erster Linie Sam Stephenson, dem Autor des Prototype-Frameworks, dass wir heute elegantere Mechanismen haben, um Ruby-ähnliche Iteratoren zu verwenden. Prototype, streng genommen eine Portierung des Ruby-Kerns nach JavaScript, macht alle generischen JavaScript-Objekte kurzerhand zu Unterklassen einer neu eingeführten Enumerable-Klasse, die zahlreiche neue Funktionen bereithält, darunter Iteratoren und Reflection-Funktionen. Patterns beschreiben Problem-Lösungs-Beziehungen und haben einige gewichtige Vorteile. Schon durch ihren Namen bietet sich eine einfache Möglichkeit zur Kommunikation und Dokumentation. Kennen mehrere Programmierer bestimmte Patterns, reicht ihr Name aus, um über Design-Alternativen zu sprechen. Design Patterns helfen, immer wieder vorkommende Probleme bei der Softwareentwicklung mit erprobten Lösungen in den Griff zu bekommen und die Software leichter verständlich zu machen. Sie sind damit ein Mittel, kostengünstig und zeitsparend Lösungen zu erarbeiten. Software, die lange verwendet werden soll, steht irgendwann vor dem Problem, dass sich mit der Zeit die Anforderungen, die sie zu erfüllen hat, ändern. Oft werden zusätzliche Anforderungen durch Erweiterungen der Software befriedigt. Dies führt aber zu einem inflexiblen System, das für spätere Anpassungen ungeeignet ist. Um die Software weiter zu entwickeln, muss sie umorganisiert werden. Dieser Prozess heißt Refactoring. Design Patterns beschreiben Strukturen, die das Resultat von Refactoring sind. Dadurch zeigen sie die Richtung, in die ein inflexibles Programm umorganisiert werden soll, um auch zukünftigen Anforderungen gewachsen zu sein. Verwendet man Design Patterns von Beginn an, wird der Bedarf für Refactoring zu einem späteren Zeitpunkt verringert oder sogar ganz vermieden. Patterns definieren die Strukturen, derer es bedarf, um das Zusammenspiel großer Klassen-Hierarchien zu regeln.
Exploring JavaScript
49
1 – Fortgeschrittene Techniken der Objektorientierung
1.10 Fazit Vielleicht liegt es am Suffix „script“, dass man JavaScript allenfalls kleinere Spielereien zutraut. Dabei hat sich JavaScript längst jene Bereiche erobert, die Hochsprachen so „hochnäsig“ machen. In den Browsern dieser Welt erbringt JavaScript Sekunde für Sekunde Höchstleistungen beim Rendern von Navigationsbäumen, bei der Manipulation von Datensätzen, bei der Kommunikation mit dem Server oder – vergessen wir nicht die Ursprünge – beim Validieren von Formulareingaben. Dass man JavaScript mehr und mehr zutraut, sieht man an den zahlreichen typischen Entwicklerwerkzeugen, die es immer häufiger auch für die JavaScript-Welt gibt: Unittest-Bibliotheken wie das in diesem Buch beschriebene JsUnit, Fenstersysteme wie Qooxdoo [4] oder DocumentationTools wie JSDoc [5] nach dem Vorbild von JavaDoc. Die ECMAScript-Spezifikation, allzu häufig Quelle von Missverständnissen, reserviert schon heute Schlüsselwörter wie class, import, super und extends. Das deutet darauf hin, dass sich künftige JavaScript-Versionen in Richtung Java und klassenbasierter Objektorientierung bewegen werden. Das Proposal für JavaScript 2.0 [6] dümpelt schon seit 2003 im Netz, ist jedoch bislang nur teilweise Realität geworden – und auch nur für Actionscript-Programmierer. Warum auch warten auf JavaScript 2.0? Die Sprache kennt jetzt schon Duck Typing, Closures, Lisp-ähnliche Features, Metaprogrammierung, Aspektorientierte Programmierung und vieles mehr. Für mich die ideale Lösung für die Webanwendungen der Zukunft. [1] http://javascript.crockford.com/javascript.html [2] http://prototype.conio.net/ [3] http://javascript.crockford.com/inheritance.html [4] http://qooxdoo.org/ [5] http://jsdoc.sourceforge.net/ [6] http://www.mozilla.org/js/language/js20/
50
2
Unit-Tests mit JavaScript
Von Jörg Schaible
2.1 Motivation Als JavaScript Ende 1995 im Netscape Navigator 2.0 eingeführt wurde, dachte noch keiner an die Entwicklung von kompletten Anwendungen in dieser Sprache. Größere Entwicklungen waren interessanterweise zuerst im Serverbereich zu finden, wo mit Hilfe von JavaScript Server Pages die Entwicklung von dynamischen Webseiten den Fähigkeiten der Webdesigner entgegenkommen sollte (Netscape iPlanet, BroadVision 1-to-1, Day Communiqué, IXOS Obtree). Die Standardisierung von JavaScript, die Weiterentwicklung der Browser (speziell auch bezüglich Kompatibilität und Konformität) und die Verfügbarkeit von schnellen Internetverbindungen haben die Situation grundlegend verändert. Aktuelle DOM-Implementierungen erlauben die Manipulation des dargestellten Dokuments on-the-fly. Zusätzlich ist in den wichtigsten Browsern ein Datenaustausch über XML möglich, sodass sowohl der Datenaustausch mit dem Server als auch die Darstellung der Daten optimiert werden kann. Diese Funktionalitäten werden clientseitig in JavaScript-Bibliotheken gekapselt, welche einen beträchtlichen Umfang annehmen können. Schon lange sind im Internet auf den einschlägigen Seiten viele einzelne Funktionen als so genannte Scriptlets für einzelne Effekte erhältlich. Aktuell geht der Trend zu größeren JavaScript-Bibliotheken, die dem Programmierer die unterschiedlichsten Funktionalitäten aus einer Hand liefern. Diese Bibliotheken präsentieren sich nicht mehr als eine Sammlung loser Funktionen, sondern verwenden intensiv die objektorientierten Möglichkeiten von JavaScript. Spätestens hier wird klar, dass solche größeren Script-Bibliotheken mit den gleichen Problemen kämpfen wie jedes andere Software-Projekt. Je größer die Anzahl der Schichten, je mehr Funktionen und Objekte im Einsatz sind, desto schwieriger wird es, die Funktionalität sicherzustellen. Insbesondere bei einer Script-basierten Programmiersprache, bei der ein Schreibfehler in einem Variablennamen nicht unbedingt direkt zu einem Fehler führt, sondern unter Umständen zu einer ungewollten Deklaration einer neuen Variablen, muss sich jeder beteiligte Entwickler ernsthafte Gedanken über die Erstellung und Durchführung von Testfällen machen. Exploring JavaScript
51
2 – Unit-Tests mit JavaScript
2.1.1
Testbarkeit
Wenn wir eine Anwendung schreiben, definieren wir logische Schichten der Funktionalität und schreiben Komponenten, die aufeinander aufbauen. Ganz am Ende der Entwicklung steht ein letzter Funktionstest, der bei einem Auftrag eine Abnahme durch den Kunden bedeutet. Umfasst die Anwendung eine gewisse Größe, sind oft mehrere Entwickler mit der Umsetzung der Anwendung beschäftigt. Diese können am besten arbeiten, wenn ihre Komponenten so weit wie möglich gekapselt sind und nur über definierte Schnittstellen miteinander kommunizieren. Die „natürliche“ Hierarchie wird durch ein Abhängigkeitsverhältnis zwischen den einzelnen Komponenten beschrieben und funktioniert analog zu den einzelnen logischen Schichten der Anwendung. Es gilt zyklische Beziehungen zwischen ihnen zu vermeiden, um einen Funktionsumfang zu definieren, die Wartbarkeit zu erhöhen, Tests zu vereinfachen und die Wiederverwendung der Komponenten zu erleichtern. Insbesondere die Testbarkeit des Codes darf nicht mit dem Testen selbst verwechselt werden. Das sind zwei unterschiedliche und sogar weitgehend unabhängige Aspekte der Qualitätssicherung. Das kontinuierliche Testen und der erfolgreiche Endtest signalisiert dem Anwender, dass das Endprodukt den geforderten Funktionen entspricht und beschreibt einen Zustand des Produkts. Die Testbarkeit hingegen ist Teil einer effektiven Strategie, die die über eine Schnittstelle angebotene Funktionalität verifizieren soll. Deswegen erfüllt qualitativ hochwertiger Code folgende Eigenschaften: 쐌 Keine zyklischen Abhängigkeiten zwischen den Komponenten 쐌 Entkoppelung von Funktionalität über Schnittstellen 쐌 Trennung von Daten und Diensten 쐌 Funktionale Einheiten werden separiert und können von außen getestet werden 쐌 Lebensdauer eines Objekts ist so kurz wie nötig Obwohl diese Forderungen im Hinblick auf JavaScript hochgestochen erscheinen und sicher beim Einfügen einer kleinen Funktion, die z.B. den Anwender auf eine fehlerhafte Eingabe hinweist, übertrieben ist, so sollten sie trotzdem beachtet werden, wenn diese Überprüfung schon anhand eines komplexen Musters erfolgen soll. Allein ein komplexer regulärer Ausdruck ist schon genug, um zu fordern, dass diese Funktionalität losgelöst von einem Ereignis in einem HTML-Formular getestet werden können soll. 52
JUnit für JavaScript
2.1.2
Funktionstests
In der zweiten Hälfte der neunziger Jahre wurde das eXtreme Programming bekannt. Abseits von den eingefahrenen Wegen der Top-Down-Programmierung wurde ein Programmierstil propagiert, bei dem der Entwicklungsprozess einer Anwendung nicht bis ins kleinste Detail beschrieben und vorgegeben war, sondern die groben Anforderungen wurden zusammen mit dem Kunden über ein Karteikartensystem immer weiter verfeinert und möglichst bald in funktionierende Prototypen umgesetzt. Das Ziel ist eine schnellstmögliche Rückmeldung des Kunden, der so die aktuelle Entwicklung beeinflussen kann. Um in einem so dynamischen System auf Änderungen zu reagieren, ohne dass Auswirkungen der Änderung übersehen werden, wird die Funktionalität über Funktionstests abgesichert, die gleichzeitig auch als Regressionstest dienen. In einem strikt nach den Regeln des eXtreme Programming geführten Projekt werden diese Tests noch vor dem eigentlichen Code geschrieben (was dem Kunden die Möglichkeit gibt, die Sinnhaftigkeit seiner eigenen Anforderungen zu prüfen) und danach erst die Implementierung durchgeführt, bis die Tests erfüllt werden. Wenngleich nicht alle Projekte diesen Paradigmen gefolgt sind, so haben doch diese Art der Funktionstests ihren Stammplatz in der Programmierung erhalten. Diese Entwicklung ist vor allem der Popularität des Test-Frameworks JUnit zu verdanken, welches Erich Gamma und Kent Beck entwickelt haben, um im Projekt den Anforderungen des eXtreme Programming gerecht zu werden.
2.2 JUnit für JavaScript Oft genug findet nur ein Test im Debugger statt. Der Programmierer überprüft seine Implementierung durch Ausprobieren und verfolgt im Fehlerfall den Programmablauf im Debugger, bis die Fehlerstelle lokalisiert ist. Seiteneffekte, die durch die Fehlerbehebung auftreten können, werden dabei oftmals nicht gefunden. Insbesondere wenn bereits erheblicher Zeitdruck besteht, wird von der zusätzlichen Entwicklung von Regressionstests abgesehen. Deswegen ist das erste Entwicklungsziel von JUnit die Bereitstellung eines Frameworks, mit dessen Hilfe diese Art der Regressionstests möglichst einfach und effizient implementiert werden kann, um die Hemmschwelle zu senken, diesen zusätzlichen Entwicklungsaufwand zu tätigen.
Exploring JavaScript
53
2 – Unit-Tests mit JavaScript
Das zweite Ziel ist die Entstehung von Tests, die auch über die Zeit ihren Wert nicht verlieren. Es soll jederzeit auch für Dritte möglich sein anhand der Tests zu verifizieren, ob eine Klasse oder Funktion die ursprüngliche Funktionalität erfüllt. Diese Fokussierung des Frameworks hat zur enormen Popularität von JUnit stark beigetragen. Innerhalb der Java-Welt gibt es kaum eine Entwicklungsumgebung, in der nicht in irgendeiner Weise JUnit-Tests ausgeführt werden können. Ebenso wurde das Framework in nahezu allen Programmiersprachen adaptiert oder für spezielle Aufgabenbereiche wie Datenbanktests oder XML-Validierung erweitert. Für JavaScript gibt es mehrere Adaptionen, die einen unterschiedlichen Fokus in der Implementierung setzen: 1. JsUnit von Edward Hieatt Diese JavaScript-Implementierung lehnt sich locker an die Architektur von JUnit an und ist speziell auf die Verwendung von JavaScript im Browser zugeschnitten. Das Projekt wird auf SourceForge geführt. 2. JsUnit von Jörg Schaible, dem Autor dieses Artikels Eine Implementierung, die sich unter Verwendung der objektorientierten Möglichkeiten von JavaScript sehr eng an die Struktur von JUnit anlegt. Ausführung von der Kommandozeile oder im Browser möglich. Das Projekt wird auf BerliOS geführt. 3. NUnit Eine Implementierung in C#, die aber für alle von .NET- unterstützten Sprachen verwendet werden kann, also auch JScript. Das Projekt wird auf SourceForge geführt. 4. ASTUce Diese Variante einer lockeren JUnit-Implementierung hat einen historischen Schwerpunkt auf ActionScript, arbeitet mittlerweile aber für alle JavaScriptDialekte auf dem ECMA-Standard. Das Projekt wird von buRRRn bereitgestellt. 5. Test::Unit Diese Implementierung ist Teil von script.aculo.us, welches Erweiterungen für das Prototype-Framework bereit stellt. Die nachfolgenden Beispiele wurden mit der JsUnit-Version des Autors durchgeführt. Allerdings gelten die Grundprinzipien zum Schreiben der Tests für alle Frameworks. 54
Testen mit JsUnit
2.3 Testen mit JsUnit Das JsUnit-Framework nutzt die objektorientierten Möglichkeiten von JavaScript, wie im Kapitel 8 des Benutzerhandbuch zur JavaScript-Sprache 1.5 beschrieben. Diese werden beim Schreiben von Testklassen auch benötigt und werden nachfolgend beschrieben.
2.3.1
Konventionen für den Quellcode
Um in JavaScript ein Objekt zu erzeugen, muss der new-Operator verwendet werden. Jedes Mal wird dabei implizit eine Funktion aufgerufen, die das Objekt erzeugt. Das neue Objekt ist dabei vom Typ function. Der Code dieser Funktion ist sozusagen das Äquivalent zum Konstruktor in Java oder C++: function ClassName(arg1, arg2) { BaseClass.call(this, arg1, arg2); }
Listing 2.1: Konstruktor-Äquivalent
Dabei kann eine Klassenhierarchie aufgebaut werden, wenn in einem solchen Konstruktor der Konstruktor der Vaterklasse aufgerufen wird. Nachdem der Konstruktor geschrieben wurde, können die einzelnen Methoden implementiert werden. Als Konvention sollte der Klassenname als Präfix verwendet werden. Alle Methoden müssen danach dem Prototypen der Klasse zugewiesen werden: function ClassName_f1() {} function ClassName_f2() {} ClassName.prototype = new BaseClass(); ClassName.prototype.f1 = ClassName_f1; ClassName.prototype.f2 = ClassName_f2;
Listing 2.2: Methodenzuweisung
Es ist kein Problem eine Methode zu überladen, die in einer Basisklasse definiert wurde. Die ursprüngliche Version kann über die Methode call oder apply aufgerufen werden:
Exploring JavaScript
55
2 – Unit-Tests mit JavaScript function ClassName_f1() { BaseClass.prototype.f1.apply(this, arguments); } function ClassName_f2() { BaseClass.prototype.f2.apply(this, arguments); }
Listing 2.3: Aufruf der Vater-Klasse
Alternativ können die Methoden auch direkt zugewiesen werden. Dabei können die Funktionen ebenfalls mit einem Namen versehen werden oder aber auch anonym bleiben: function ClassName(arg1, arg2) { BaseClass.call(this, arg1, arg2); } ClassName.prototype = new BaseClass(); ClassName.prototype.f1 = function ClassName_f1(arg1) { // do something } ClassName.prototype.f2 = function() { // do something }
Listing 2.4: Anonyme Funktionen
Vorteil dieser Variante ist die etwas kompaktere Schreibweise. Die Verwendung von anonymen Funktionen hat aber zur Folge, dass diese Funktionen bei den JavaScript-Engines, die einen Stacktrace unterstützen, nicht mehr aufgelöst werden können und so die Fehleranalyse erschweren. JsUnit unterstützt auch einen Modus, bei dem die definierten Methoden automatisch dem prototype zugewiesen werden. Hierzu wurde die JavaScript-Klasse Function um eine Methode glue erweitert, die diese Zuordnung für alle Funktionen vornimmt, die dem Namensschema ClassName_method entsprechen:
56
Testen mit JsUnit function ClassName(arg1, arg2) { BaseClass.call(this, arg1, arg2); } ClassName.prototype = new BaseClass(); function ClassName_f1(arg1) { // do something } function ClassName_f2() { // do something } ClassName.glue();
Listing 2.5: Automatisierte Methodenzuweisung
Beim Zusammenfügen wird der Name der Funktion abzüglich des Präfix (Klassenname inkl. Unterstrich) als Name des neuen prototype Elements verwendet. Nachteil der Variante ist eine Erhöhung der Laufzeit, weil jedes Mal alle Objekte im globalen Namensraum durchsucht werden müssen. Für das Zusammenbauen der Testklassen ist die Laufzeit aber eher zweitrangig, wichtig ist hier, dass keine Methode vergessen und somit ein Teil der Funktionalität unbemerkt ungetestet bleibt. Da JUnit auch Gebrauch von Interfaces macht, für solche Objekte in JavaScript aber keine direkte Entsprechung existiert (obwohl implements ein Schlüsselwort ist), definieren wir diese ebenfalls per Konvention. Das Interface selbst kann sehr minimalistisch implementiert werden, denn die Funktionen werden nie aufgerufen: function InterfaceName() {} InterfaceName.prototype.f1 = function() {} InterfaceName.prototype.f2 = function(a, b) {}
Listing 2.6: Schnittstellendefinition
Für die Überprüfung wurde die Klasse Funktion von JavaScript um die Methode fulfills erweitert, mit der sichergestellt werden kann, dass eine Klasse ein solches Interface erfüllt:
Listing 2.7: Sicherstellung der Implementierung der Schnittstelle
2.3.2
Erstellen von Testklassen
Um Funktionstests zu schreiben, müssen zuerst Testklassen implementiert werden. Wie in JUnit wird auch bei JsUnit eine solche Klasse von TestCase abgeleitet: function SimpleTest(name) { TestCase.call(this, name); } SimpleTest.prototype = new TestCase();
Listing 2.8: Einfache Testklasse
Die setUp Methode kann bei Bedarf überladen werden (ebenso wie die Methode tearDown): function SimpleTest_setUp() { TestCase.prototype.setUp.apply(this, arguments); this.fValue1 = 2; this.fValue2 = 3; }
Listing 2.9: Testinitialisierung
Danach können alle Testmethoden implementiert werden. Wie bei JUnit besteht auch hier die Konvention, dass die Namen dieser Methoden mit test beginnen: function SimpleTest_testDivideByZero() { var zero = 0;
Listing 2.10: Testmethoden
58
Testen mit JsUnit this.assertEquals("Infinity", 8/zero); } SimpleTest.glue();
Listing 2.10: Testmethoden (Forts.)
Die Zuweisung der Methoden zur Klasse erfolgt hier implizit durch die Verwendung der glue-Methode. Der nächste Schritt besteht in der Erstellung einer eigenen Testsuite. Diese ist ebenfalls wie in JUnit von der Klasse TestSuite abgeleitet: function SimpleTestSuite() { TestSuite.call(this, "SimpleTestSuite"); this.addTestSuite(SimpleTest); } SimpleTestSuite.prototype = new TestSuite(); SimpleTestSuite.prototype.suite = function() { return new SimpleTestSuite(); }
Listing 2.11: Testsuite
Im Konstruktor werden die Testklassen über deren Namen hinzugefügt. Dabei können auch die Namen von anderen Testsuiten übergeben und die Suiten somit verschachtelt werden. Die einzelnen Test-Methoden werden ebenfalls wie in JUnit anhand der Namenskonvention gefunden. Die Implementierung der Testsuiten ist nicht unbedingt notwendig. JsUnit kommt mit verschiedenen Implementierungen des TestCollector-Interfaces, welches verwendet werden kann, um die Namen der Testklassen über den globalen Namensraum zu ermitteln. Die Testsuiten werden dann von den BaseTestRunner-Implementierungen automatisch erzeugt. Trotzdem kann es notwendig sein, eine Testsuite zu implementieren, z.B. wenn alle Testfunktionen einer Testklasse mit unterschiedlicher Initialisierung durchgeführt werden sollen. Im Gegensatz zu Java werden im Normalfall in JavaScript sämtliche Klassen und Funktionen, die eine Einheit bilden, in eine einzelne Datei geschrieben, um die zu ladenden Dateien klein zu halten. Deswegen stehen normalerweise auch eine ganze Reihe von einzelnen Testfällen in einer Datei. Die typische 1:1-Beziehung in Java von Klasse und Datei fehlt hier. Mit einzelnen Testsuiten können diese dann wieder gruppiert und separat über deren Namen adressiert werden. Exploring JavaScript
59
2 – Unit-Tests mit JavaScript
2.3.3
Ausführung
Für das Ausführen der Tests kommt es sehr auf die Laufzeitumgebung an. Ein Teil der benötigten Basisfunktionalität ist unabhängig, andere Teile unterscheiden sich insbesondere beim Aufruf. Je nachdem, ob die Tests im Browser eingebettet sind, von der Kommandozeile gerufen, Server-seitig eingebunden oder während eines Bauprozesses mit Ant oder Maven angestoßen werden, müssen unterschiedliche Vorbereitungen getroffen werden. Für den unabhängigen Teil existiert wie in JUnit eine Klasse BaseTestRunner, die als Basisklasse für die unterschiedlichen Varianten dient. Sie verwendet eine Implementierung eines TestListener, der für das Ergebnis und die Dauer der einzelnen Tests aufgerufen wird. Es existieren in JsUnit zwei Ableitungen, die eine textorientierte Ausgabe unterstützen. Für die Verarbeitung von Kommandozeilenparametern ist die Klasse TextTestRunner verfügbar: var result = TextTestRunner.prototype.main(args); JsUtil.prototype.quit(result);
Listing 2.12: Ausführung eines TextTestRunners
Aktuell werden folgende Optionen unterstützt: 쐌 --classic Ausführliche Ausgabe während des Tests. Dies war die einzige Ausgabemöglichkeit bei Version 1.0. 쐌 --xml Ausgabe als XML im Format für JUnitReport 쐌 --html Aufbereitung des Ausgabetextes für HTML 쐌 --run TESTCASES|TESTSUITES Ausführen aller TestCase- bzw. TestSuite-Klassen, die im globalen Namensraum zu finden sind Wird weder die Option --xml noch --classic angegeben, erfolgt eine sehr kompakte Ausgabe, wie sie auch bei JUnit 3.8.1 zu finden ist. Als weitere Parameter folgen die Namen der Testklassen oder -suiten, die ausgeführt werden sollen. Ohne die Option --run und ohne Argument wird eine Testsuite mit dem Namen AllTests gesucht und ausgeführt. 60
Testen mit JsUnit
Die andere zur Verfügung stehende Implementierung ist die Klasse EmbeddedTestRunner. Diese benötigt natürlich keine main-Methode, die Optionen verarbeitet, sondern über den Konstruktor wird der ResultPrinter gesetzt und der runMethode werden die Namen der auszuführenden Tests oder Testsuiten übergeben: var printer = new XMLResultPrinter(); var runner = new EmbeddedTextTestRunner(printer); var collector = new TestSuiteCollector(this); runner.run(collector.collectTests());
Listing 2.13: Initialisierung und Ausführung eines EmbeddedTestRunners
In diesem Code-Ausschnitt werden die Namen der Testsuiten zusammengesucht und das Ergebnis wird in XML ausgegeben. JavaScript ist von Haus aus als Sprache konzipiert, die in eine andere Anwendung eingebunden wird. Deswegen gibt es im Sprachstandard keine Möglichkeit, auf Dateien zuzugreifen bzw. Programmcode zu laden und auszuführen. Innerhalb des Browsers ist dies kein Problem, HTML bietet die Möglichkeit, verschiedene JavaScript-Dateien zu laden. Bei den JavaScript-Engines jedoch, die auch von der Kommandozeile aus aufgerufen werden, ist diese Funktionalität meist zwar vorhanden, aber naturgemäß immer als proprietäre Erweiterung. Das Gleiche gilt für die Beendigung des Scripts mit einem Rückgabewert, welcher der aufrufenden Shell oder der Anwendung zurückgeben wird. JsUnit enthält eine Abstraktionsschicht über diese Operationen. Die Bibliothek erkennt die JavaScript-Variante anhand von proprietären Erweiterungen und kapselt die benötigten Funktionalität. Dabei können nicht alle erwünschten Features von allen Engines unterstützt werden; insbesondere die Ermittlung des Call-Stacks im Fehlerfall ist eine Spezialität von mozilla.orgs SpiderMonkey und Microsofts JScript und baut auf eine Funktionalität, die im aktuellen Sprachstandard leider als „deprecated“ gekennzeichnet ist und in zukünftigen Versionen der Engines nicht mehr unterstützt werden muss. Die Hauptfunktionalität des Frameworks selbst ist in zwei einzelnen Dateien implementiert: 쐌 lib/JsUnit.js: Die Umsetzung der Klassen aus JUnit in JavaScript 쐌 lib/JsUtil.js: Die Funktionalität für die Abstraktionsschicht JsUnit.js baut dabei auf der Funktionalität von JsUtil.js auf. Der Code beider Dateien muss nacheinander geladen werden, um die Tests ausführen zu können.
Exploring JavaScript
61
2 – Unit-Tests mit JavaScript
JsUnit enthält zum einen die portierten Beispiele von JUnit und auch umfangreiche Tests zur Sicherstellung der eigenen Funktionalität, welche alle in den unterstützten Laufzeitumgebungen ausgeführt werden können. Die wichtigsten dieser Umgebungen im Überblick: 쐌 Kommandozeile Browser-unabhängiger Code lässt sich sehr gut über die Kommandozeile testen. Dazu können verschiedene JavaScript-Engines verwendet werden. Sowohl für die Beispiele als auch für die Unit Tests von JsUnit selbst existiert eine Datei AllTests.js, die als Argument auf der Kommandozeile übergeben werden kann. Die weiteren Optionen und Argumente, die von der Klasse TextTestRunner unterstützt werden, werden einfach beim Aufruf des Kommandozeileninterpreters angehängt. Lediglich der JavaScript-Interpreter von KDE unterstützt keine zusätzlichen Argumente für das Script. JavaScript-Engine
Dabei lässt sich das aufgerufene Script so kapseln, dass alle Engines den Code ausführen können (allerdings muss JsUtil.js zuerst mit den proprietären Erweiterungen geladen werden, bevor die Kapselung dieser Funktionalität zur Verfügung steht). Nachfolgend das Script AllTests.js, mit dem aus dem Beispielverzeichnis von JsUnit heraus die Tests ausgeführt werden können: if(this.WScript) { var fso = new ActiveXObject( "Scripting.FileSystemObject"); var file = fso.OpenTextFile( "../lib/JsUtil.js", 1);
Listing 2.14: Ausführen der Tests von der Kommandozeile
62
Testen mit JsUnit var all = file.ReadAll(); file.Close(); eval(all); } else { load("../lib/JsUtil.js"); } eval(JsUtil.prototype.include("../lib/JsUnit.js")); eval(JsUtil.prototype.include("ArrayTest.js")); // weitere Dateien mit Tests und Testsuiten function AllTests() { TestSuite.call(this, "AllTests"); } function AllTests_suite() { var suite = new AllTests(); suite.addTest( ArrayTestSuite.prototype.suite()); // weitere Tests und Test-Suites return suite; } AllTests.prototype = new TestSuite(); AllTests.prototype.suite = AllTests_suite; var args; if(this.WScript) { args = new Array(); for(var i=0; i<WScript.Arguments.Count(); ++i) { args[i] = Wscript.Arguments(i); } } else if(this.arguments) { args = arguments; }
Listing 2.14: Ausführen der Tests von der Kommandozeile (Forts.)
Exploring JavaScript
63
2 – Unit-Tests mit JavaScript else { args = new Array(); } var result = TextTestRunner.prototype.main(args); JsUtil.prototype.quit(result);
Listing 2.14: Ausführen der Tests von der Kommandozeile (Forts.)
TIPP cscript kann auch unter Linux mit Hilfe von WINE innerhalb der Konsole
gestartet werden – eine (nicht selbstverständliche) Installation des Windows Scripting Host vorausgesetzt. 쐌 Browser Innerhalb des Browsers lässt sich der JavaScript-Code am besten durch die Erstellung einer HTML-Seite testen, die sowohl die Funktionalität von JsUnit als auch die einzelnen Tests bzw. Testsuiten inkludiert: JsUnit Test <script type="text/javascript" language="JavaScript" src="../lib/JsUtil.js"/> <script type="text/javascript" language="JavaScript" src="../lib/JsUnit.js"/> <script type="text/javascript" language="JavaScript" src="ArrayTest.js"/> <script type="text/javascript" language="JavaScript" src="AllTests.js"/>
JsUnit Test
<script language="JavaScript1.3"> var writer = new HTMLWriterFilter( JsUtil.prototype.getSystemWriter()); var printer = new ClassicResultPrinter(writer);
Listing 2.15: Ausführen der Tests im Browser
64
Testen mit JsUnit var runner = new EmbeddedTextTestRunner(printer); var collector = new TestCaseCollector(this); runner.run(collector.collectTests());
Listing 2.15: Ausführen der Tests im Browser (Forts.)
Das Laden dieser Seite im Browser führt das Script mit der Initialisierung und Durchführung der Tests im Body der HTML-Seite aus. Der EmbeddedTextTestRunner wird dabei mit der klassischen Version des ResultPrinter bestückt, die bei der Ausführung der Tests etwas informativer ist. Die Ausgabe erfolgt auf dem SystemWriter (d.h. einer Implementierung, die im Browser in das aktuelle Dokument schreibt), welcher von einer Klasse gefiltert wird, die die Textausgabe für HTML aufbereitet. Für den Microsoft Internet Explorer existiert auch ein Beispiel (IE.js), mit dessen Hilfe die Tests von der Kommandozeile so ausgeführt werden, dass der Internet Explorer automatisch mit der Seite AllTests.html gestartet und das Testergebnis über die Windows-Automatisierung direkt aus dem Browser abgeholt und auf der Konsole ausgegeben wird. 쐌 Ant Ant von der Apache Software Foundation ist eine Bauumgebung für Projekte, die speziell im Java-Bereich einen weit verbreiteten Einsatz findet. Die Bauvorschrift befindet sich in einer Datei mit dem Namen build.xml. Diese Datei definiert Ziele, die direkt über die Kommadozeile aufgerufen werden können. Jedes Ziel wird durch eine Reihe von Kommandos, den so genannten Tasks, beschrieben. So ein Task gibt es auch für JsUnit, welcher Rhino von mozilla.org als JavaScript-Engine einbindet und die Testergebnisse in XML-Dateien festhält, die vom JUnitReport-Task weiterverarbeitet und zu einem ordentlichen Bericht umgewandelt werden können: <project name="JsUnit.samples" default="test"> <property name="build.dir" location="../target"/>
In diesem Ant-Script wird zuerst ein Task für JsUnit definiert. Für diesen Task werden die zu testenden JavaScript-Dateien und die Reihenfolge, in der sie geladen werden, mit Hilfe des source-Elements bestimmt (die JavaScript-Dateien von JsUnit sind bereits automatisch geladen und müssen nicht angegeben werden). Danach können verschiedene Testsuiten definiert werden. Der type-Parameter bestimmt den TestCollector und kann die Werte ALLTESTS, TESTSUITES oder TESTCASES annehmen. Die Dateien mit den Tests und Testsuiten werden über ein fileset-Element zur Verfügung gestellt, da diese Dateien unabhängig voneinander sein sollten. Für die Ausführung wird einfach in der Kommandozeile ant aufgerufen. Die Java-Bibliotheken jsunit.jar, jsunit-ant.jar und js.jar müssen sich im Classpath von Ant befinden. Es gibt aber eine Unzahl von weiteren Projekten, die Ant-Tasks zur eigenen Erweiterung ihrer Funktionalität unterstützen. 쐌 Maven 2 Maven, ebenfalls von der Apache Software Foundation, ist ebenso wie Ant eine Bauumgebung für Projekte, die ebenfalls im Java-Bereich verbreitet ist. Im Gegensatz zu Ant ist das Projekt-Layout bei Maven recht starr und damit uniform und hat weitere Stärken bei der Report-Generierung und der Verwaltung der Abhängigkeiten. Die Projektbeschreibung befindet sich in einer Datei mit dem Namen pom.xml. Diese Datei definiert unter anderem auch zusätzliche Plugins, die automatisch bei der Ausführung der Ziele zu einer Phase des so genannten Lebenszyklus ausgeführt werden. Solch ein Plugin für Maven 2 gibt es auch für JsUnit, welches in der Testphase gestartet wird. Gleich dem Ant-Task wird Rhino von mozilla.org als JavaScript-Engine eingebunden und die Testergebnisse werden ebenfalls im XML-Format festgehalten, welches vom Surefire-Report-Plugin weiterverarbeitet und zu einem ordentlichen Bericht umgewandelt werden kann: <project> <modelVersion>4.0.0 de.berlios.jsunit <artifactId>jsunit-samples SNAPSHOT <packaging>jar JsUnit Samples
Listing 2.17: Einbinden der Tests in Maven 2
Exploring JavaScript
67
2 – Unit-Tests mit JavaScript de.berlios.jsunit <artifactId> jsunit-maven2-plugin <executions> <execution> test <sourceDirectory>. <sources> <source>money/IMoney.js <source>money/Money.js <source>money/MoneyBag.js money ${project.build.directory}/surefire-reports Maven2TestSuiteTESTCASES*.jsjsunit-test
Listing 2.17: Einbinden der Tests in Maven 2 (Forts.)
68
Testen mit JsUnit org.apache.maven.plugins <artifactId> maven-surefire-report-plugin
Listing 2.17: Einbinden der Tests in Maven 2 (Forts.)
In dieser Projektbeschreibung wird das JsUnit-Plugin zuerst an die Phase „test“ gebunden. Dann erfolgt die Angabe des Vezeichnisses, in das die XML-Reports geschrieben werden. Hier muss das gleiche Verzeichnis verwendet werden, in das das Surefire-Plugin schreibt, welches die Tests unter Java ausführt. Danach werden ähnlich wie beim Ant-Task die einzelnen JavaScript-Dateien mit dem zu testenden Code angegeben, gefolgt von den Testsuiten. Auch hier gibt es einen Typ, der die gleichen Werte wie beim Ant-Task annehmen kann. Über das includes-Element werden dann wieder die Testsourcen bestimmt. Für die Ausführung der Tests wird einfach in der Kommandozeile mvn test aufgerufen. Mit mvn site werden sogar gleich noch die Reports erstellt. Maven sorgst selbst dafür, dass die benötigten Java-Bibliotheken zur Verfügung stehen.
2.3.4
Testmethoden
Die einzelnen Tests werden über eine Reihe von Testmethoden (Fixtures im Sprachgebrauch von JUnit) erstellt. Der Name einer jeden Methode muss per Konvention mit „test“ beginnen, damit sie automatisch Teil der Testsuite wird. Jede Instanz einer Testklasse führt genau eine der Methoden als Test aus. Vor dem Test wird noch die Methode setUp ausgeführt, nach dem Test die Methode tearDown, unabhängig davon, ob der Test fehlgeschlagen ist. Im Test selbst verwendet man nun die verschiedenen geerbten Methoden der Klasse TestCase, die diese selbst von der Klasse Assert erbt. In den Testmethoden werden Annahmen getroffen in Form von Methoden, die mit dem Wort „assert“
Exploring JavaScript
69
2 – Unit-Tests mit JavaScript
beginnen. Diese Methoden sind denen von JUnit nachempfunden, gehen aber auf die Möglichkeiten von JavaScript ein (bei allen ist der hier demonstrierte erste Parameter mit der erklärenden Meldung aber optional): 쐌 assertTrue: Der einfachsten dieser Methoden wird eine Bedingung übergeben, die zur Laufzeit richtig sein muss: assertTrue("No browser", this.window != null);
쐌 assertFalse: Das Gegenstück zu assertTrue; die Bedingung muss zur Laufzeit falsch sein: assertFalse("No shell", this.window == null);
쐌 assertNull: Die Überprüfung eines Objekt auf null: assertNull("Object not null", this.window);
쐌 assertNotNull: Die Überprüfung eines Objekt auf ungleich null: assertNotNull("Object is null", this.window);
쐌 assertSame: Die Überprüfung einer Referenz auf dasselbe Objekt (Operator ===): assertSame("Objects not the same", this, o);
쐌 assertUndefined: Die Überprüfung auf ein nicht initialisiertes Objekt: assertUndefined("Object is defined", fn());
쐌 assertNotSame: Sicherstellung von verschiedenen Referenzen trotz möglichem gleichen Wert: assertNotSame("Object is same", "x", "x");
쐌 assertEquals: Der Vergleich mit einem vorgegebenen Objekt oder Wert (der erwartete Wert zuerst): assertEquals("Wrong array size", 5, myArray.length);
Sind beide Objekte Strings, so werden nur die Unterschiede ausgegeben. 쐌 assertFloatEquals: Die spezialisierte Ausgabe von assertEquals für FloatWerte, bei der ein Toleranzbereich angegeben wird, um der prinzipiellen ungenauen Darstellungsmöglichkeit von Fließkommazahlen bei Prozessoren Rechnung zu tragen: 70
Beispiele für Unit Tests assertFloatEquals("Is not 1", 1, 0.99, 0.1);
쐌 assertMatches: Die Überprüfung eines Strings anhand eines regulären Ausdrucks: assertMatches("Not a digit", /\d/, "1");
Manchmal soll der Test auch fehlschlagen, wenn während der Ausführung des Testcodes eine bestimmte Stelle erreicht wird, wozu die Methode fail verwendet werden kann: fail("Should not execute this", new CallStack());
2.4 Beispiele für Unit Tests Nachfolgend ein paar Beispiele, die bei der Umsetzung von Unit Tests helfen sollen. Wie immer sollte der zu testende Code isoliert sein, d.h. nicht als Teil einer onEvent-Methode des Browsers, sondern in Form einer separaten Funktion, die von dort aus aufgerufen wird. Da JavaScript eine interpretierte Sprache ist, empfiehlt es sich außerdem den Testcode in einer separaten Datei zu verwalten, damit der Interpreter der Engine im Live-System nicht unnötig mit Code belastet wird bzw. unnötige Ladezeiten im Browser entstehen.
2.4.1
Test einzelner Funktionen
Eine im Browser oft verwendete Funktionalität ist die Validierung von Eingabefeldern. Da die Überprüfung der Feldinhalte oft Wiederverwendung finden kann, können die einzelnen Validierungsfunktionen in eine separate Bibliothek von einzelnen Funktionen überführt werden. Als einfaches Beispiel eine Funktion zur Validierung einer E-Mail-Adresse. Hierzu erzeugen wir eine Funktion verifyEmail, die anhand eines regulären Ausdrucks für eine gültige E-Mail-Adresse true zurück gibt, andernfalls false. Hier unsere erste etwas naive Version: function validateEmailAddress(address) { return /^[\w.]+@\w+(\.\w+)+)$/.test(address); }
Listing 2.18: Zu testende Methoden
Exploring JavaScript
71
2 – Unit-Tests mit JavaScript
Wir erlauben im Namensteil alphanumerische Zeichen, den Unterstrich und Punkt und fordern mindestens ein Zeichen. Nach dem obligatorischen at-Zeichen folgt die Domain mit mindestens einem Punkt zwischen alphanumerischen Zeichen. Der reguläre Ausdruck sieht schon recht komplex aus, aber funktioniert er auch wie gewollt? Hier empfiehlt sich die Entwicklung eines Testfalls mit einigen Testfunktionen, die den Ausdruck auf Herz und Nieren prüfen: function EmailValidatorTest(name) { TestCase.call(this, name); } function EmailValidatorTest_testStandardEmail() { this.assertTrue( validateEmailAddress("[email protected]")); } function EmailValidatorTest_testEmailToLocalhost() { this.assertTrue( validateEmailAddress("root@localhost")); } function EmailValidatorTest_testEmailHasAnAtSign() { this.assertFalse( validateEmailAddress("john.doe.AT.acme.org")); } function EmailValidatorTest_testEmailUsesASCII7Charcters() { this.assertFalse( validateEmailAddress("jörg@localhost")); } function EmailValidatorTest_testDomainHasARoot() { this.assertFalse(
Listing 2.19: Testklasse mit Fixtures
72
Beispiele für Unit Tests validateEmailAddress("john.doe@noroot")); } function EmailValidatorTest_testDomainRootHasAtLeastTwoCharacters() { this.assertFalse( validateEmailAddress("[email protected]")); } function EmailValidatorTest_testNameMayNotEndWithDot() { this.assertFalse( validateEmailAddress("[email protected]")); } function EmailValidatorTest_testNameMayNotStartWithDot() { this.assertFalse( validateEmailAddress("[email protected]")); } function EmailValidatorTest_testNameMustExist() { this.assertFalse(validateEmailAddress("@test.x")); } function EmailValidatorTest_testDomainMustExist() { this.assertFalse( validateEmailAddress("joehn.doe@")); } function EmailValidatorTest_testUndefinedArgumentAsAddress() { this.assertFalse(validateEmailAddress()); } function EmailValidatorTest_testEmptyAddress() { this.assertFalse(validateEmailAddress(""));
Listing 2.19: Testklasse mit Fixtures (Forts.)
Exploring JavaScript
73
2 – Unit-Tests mit JavaScript } EmailValidatorTest.prototype = new TestCase(); EmailValidatorTest.glue();
Listing 2.19: Testklasse mit Fixtures (Forts.)
Diese Testklasse zeigt, dass es eine ganze Reihe von Möglichkeiten gibt, eine ungültige E-Mail-Adresse einzugeben (obwohl es sicher noch weitere Varianten gibt). Und wie fast schon erwartet, hält unsere ursprüngliche Funktion zum Überprüfen der Adresse diesen Tests nicht stand. Die Fassung der Funktion, die mit all diesen Testfällen zurecht kommt, sieht wie folgt aus: function validateEmailAddress(address) { var r = /^(\w[\w.]*)*\w+@(localhost|\w+(\.\w{2,})+)$/; return r.test(address); }
Listing 2.20: Verbesserte Methoden nach dem Test
An diesem Beispiel lassen sich aber noch ein paar Grundprinzipien für das Schreiben einer Testklasse erklären: 1. Verwendung von sprechenden Namen. Insbesondere bei JavaScript sollten die Namen der Testfunktionen sprechend sein, damit man sofort erkennen kann, in welchem Test ein Fehler auftritt. 2. Jede Testfunktion sollte nur einen Aspekt der Funktionalität prüfen. Würden in diesem Beispiel alle Annahmen (assert-Aufrufe) in einer einzigen Testfunktion testVerifyEmailAddress zusammengefasst, wäre im Fehlerfall nur im Debugger zu ermitteln, welche Annahme denn nun fehl schlägt. 3. Überprüfung von programmiertypischen fehlerhaften Argumenten. Bei JavaScript sind dies der leere String und nicht definierte Argumente. 4. Möglichst früh mit dem Schreiben der Tests beginnen und ausführen. So lässt sich viel leichter feststellen, wo eine Funktion noch Defizite hat.
2.4.2
Test von Klassen
Als gängige Praxis hat sich die Erstellung einer Testklasse pro Klasse erwiesen. Oft wird auch pro Methode eine entsprechende Testmethode verwendet. Dieser zweite in Java weit verbreitete Ansatz erweist sich für JavaScript als nicht sehr 74
Beispiele für Unit Tests
praktikabel, insbesondere wegen der im Vergleich zu anderen Programmiersprachen eingeschränkten Debugging-Fähigkeiten. Deshalb sollten auch bei Klassen die Testmethoden eher sprechend sein, um im Fehlerfall die fehlgeschlagene Funktionalität leichter identifizieren können (dieses Prinzip beherzigen die Tests für JsUnit leider selbst nicht, diese Erkenntnis wuchs auch beim Autor erst im Laufe der Zeit auf leidvolle Art). Gerade bei Objekten ist auf die am Anfang des Kapitels angesprochene Testbarkeit zu achten. Als Beispiel soll hier noch einmal auf die Initialisierung des EmbeddedTextTestRunners hingewiesen werden: var writer = new StringWriter(); var printer = new XMLResultPrinter(writer); var runner = new EmbeddedTextTestRunner(printer); var collector = new TestSuiteCollector(this); runner.run(collector.collectTests());
Listing 2.21: Initialisierung eines EmbeddedTextTestRunners
Die einzelnen Objekte werden im globalen Kontext angelegt und miteinander verbunden, indem sie den einzelnen Konstruktoren oder den Funktionen der Objekte übergeben werden. Die Klassen legen nur dann eigene Standardobjekte an, wenn die Argumente beim Aufruf fehlen. Was hat dies nun mit der Testbarkeit zu tun? Die Übergabe der Objekte, mit denen gearbeitet wird, ermöglicht die Verwendung von Stellvertretern, so genannten Mock-Objekten. Ein Mock-Objekt verhält sich gegenüber dem zu testenden Objekt völlig transparent, d.h., das getestete Objekt merkt keinen Unterschied. Dies kann genutzt werden, um durch Rückgabewerte den Ablauf des zu testenden Objekts zu steuern oder aber um zu prüfen, ob bestimmte Funktionen des Stellvertreters aufgerufen wurden. Zur Demonstration erzeugen wir eine Klasse, die ein Eingabefeld in einem Browser verwalten kann. Das Feld verwendet einen Validator, im Beispiel wird ein E-Mail-Validator implementiert, wie im letzten Abschnitt entwickelt: function Validator() { } Validator.prototype.validate = function(text) {}
Listing 2.22: Beispiel für ein Feld mit E-Mail-Validierung
Exploring JavaScript
75
2 – Unit-Tests mit JavaScript function EmailValidator() { } EmailValidator.prototype = new Validator(); EmailValidator.prototype.validate = validateEmailAddress; EmailValidator.fulfills(Validator); function ValidatingFieldElement(element, validator) { element.fieldReference = this; element.onChange = function () { this.fieldReference.callback(); } element.bgColor = "#FF0000"; this.validator = validator; this.element = element; } function ValidatingFieldElement_callback() { this.element.bgColor = this.validator.validate(this.element.value) ? "#00FF00" : "#FF0000"; } ValidatingFieldElement.prototype.callback = ValidatingFieldElement_callback;
Listing 2.22: Beispiel für ein Feld mit E-Mail-Validierung (Forts.)
Der Test mit einem realen Objekt könnte nun aussehen wie folgt (wobei das Feld mit dem Inhalt selbst bereits ein Stellvertreter-Objekt ist, schließlich haben wir in den Kommandozeilen-Versionen von JavaScript kein DOM): function ValidatingElementTest_testWithEmailValidator() { var element = new Object(); var validator = new EmailValidator();
Listing 2.23: Test mit Stellvertreter mit DOM-Element
76
Beispiele für Unit Tests var field = new ValidatingFieldElement( element, validator); this.assertEquals("#FF0000", element.bgColor); element.value = "[email protected]"; element.onChange(); this.assertEquals("#00FF00", element.bgColor); }
Listing 2.23: Test mit Stellvertreter mit DOM-Element (Forts.)
Hier erfolgt jetzt ein Test mit Hilfe der Klasse EmailValidator. Als Alternative jetzt eine Version mit Hilfe eines Stellvertreters als Validator: function ValidatingElementTest_testWithMock() { var element = new Object(); var validator = new Validator(); validator.validate = function () { Assert.prototype.assertEquals("demo"); this.wasCalled = true; } var field = new ValidatingFieldElement( element, validator ); this.assertEquals("#FF0000", element.bgColor); element.value = "demo"; element.onChange(); this.assertTrue(validator.wasCalled); this.assertEquals("#00FF00", element.bgColor); }
Listing 2.24: Test der reinen Funktionalität
Das Problem bei der ersten Variante ist, dass wir nicht nur testen, ob der Callback der zu testenden Klasse funktioniert, sondern auch gleich noch den Validator selbst. Ändert sich nun dessen Implementierung, so kann unter Umständen unser Test fehlschlagen, obwohl sich die zu testende Klasse selbst weder verändert hat noch fehlerhaft funktioniert. Ebenfalls wird in diesem Beispiel deutlich, dass sich auch die DOM-Objekte für einen Test sehr einfach simulieren lassen. Schließlich soll jetzt in erster Linie nicht der Browser getestet werden, sondern die Funktion der Implementierung.
Exploring JavaScript
77
2 – Unit-Tests mit JavaScript
Der Test in den verschiedenen Browsern folgt zu einem späteren Zeitpunkt und dort sollten wirklich nur noch die Unterschiede durch die Browser selbst zum Vorschein kommen, nicht aber fehlerhafte Implementierungen der Funktionalität zuschlagen, denn nicht immer kann jeder Browser unter jeder Plattform getestet werden. Als Zusammenfassung: 1. Für die Testbarkeit Objekte möglichst immer übergeben, nicht in den Klassen selbst erzeugen. 2. Im Unit Test möglichst nur den Code des zu testenden Objekts prüfen, nicht aber der Objekte, die implizit verwendet werden. 3. Der Einsatz von Stellvertretern erlaubt die Fokussierung des Tests.
2.4.3
Testen von Exceptions
Ebenfalls sollte bei den Unit Tests der Code, der für die Fehlerbehandlung zuständig ist, nicht zu kurz kommen. Dabei muss dann im Unit Test oft überprüft werden, ob eine bestimmte Exception geworfen wurde. Da dieser Mechanismus in JavaScript nur ein allgemeines Abfangen einer Exception unterstützt, nicht aber ein bestimmter Typ, kommt hier ein Code-Pattern zum Einsatz, denn das Problem ist hierbei, dass auch JsUnit im Fehlerfall selbst eine Exception wirft: for(;;) { try { var x = y; } catch(ex) { break; } fail("Exception should have been raised", new CallStack()); }
Listing 2.25: Code-Pattern zum Test von Exceptions
Mit Hilfe der Schleife wird sichergestellt, dass der Test nur dann erfolgreich durchlaufen werden kann, wenn im try-Block eine Exception geworfen wird. 78
Literaturhinweise
Als Alternative bietet sich noch die Verwendung des ExceptionTestCase an, der auch dann fehl schlägt, wenn im try-Block ein AssertionFailedError geworfen wurde anstatt eines anderen beliebigen Objekts.
2.5 Literaturhinweise 1. Lakos, John: Large-Scale C++ Software Design, 1996, Addison-Wesley, ISBN 0-201-63362-0 2. Beck, Kent und Gamma, Erich: JUnit A Cook's Tour, http://junit.sourceforge.net/junit3.8.1/doc/cookstour/cookstour.htm 3. Schaible, Jörg: JsUnit, 1999-2006, http://jsunit.berlios.de/ 4. Netscape Communications Corp.: Core JavaScript Guide 1.5, 2000, http:// devedge-temp.mozilla.org/library/manuals/2000/javascript/1.5/guide/
Exploring JavaScript
79
3
AJAX
Von Arne Blankerts Ich bin aus vollster Brust modern und hoffe, man sieht es mir an (Erich Kästner) Hört man sich heute nach aktuellen Trends und Technologien im WWW um, ist einer der ersten genannten Begriffe „Web 2.0“, dicht gefolgt von „AJAX“. Doch trotz des Marketinghypes um die Themen ist technisch gesehen nichts davon neu. Genau genommen handelt es sich hier ausschließlich um eine derzeit gebräuchliche Zusammenstellung verschiedener Technologien und Nutzungsarten von Webseiten. Eine Übersetzung des Begriffs Web 2.0 könnte in etwa lauten: Es handelt sich bei einer Site aus dem Web 2.0-Umfeld um eine Homepage, die direkte Interaktion von Usern untereinander ermöglicht, eine gewisse soziale Vernetzung etabliert und es den Besuchern auch ohne ausgefeilte HTML-Kenntnisse ermöglicht, Inhalte ins World Wide Web zu bringen, um sie dort anderen zugänglich zu machen und/oder sich dort mit anderen darüber auszutauschen. Waren vor kurzem noch Foren und bestenfalls Webchat-Systeme das Maß der Dinge, geht es im Web 2.0 um schnelle, zielgerichtete Interaktion zwischen Benutzer und Benutzer sowie zwischen Benutzer und der Webseite selbst. Den Benutzer interessiert dabei eher wenig, wie die Technik funktioniert, die ihm diese Interaktion ermöglicht – im Normalfall interessiert ihn ausschließlich, dass eine Webseite die Tools zur Verfügung stellt, die er benötigt, um eine bestimmte Idee umzusetzen. Dabei kann es sich um ein Blog, eine Foto-Community, ein Wiki, ein E-Mail-Konto mit interaktiver Benutzeroberfläche oder eine Kontaktbörse handeln. Wissen, Ideen und Materialien werden zur Verfügung gestellt, mit anderen geteilt und synergetisch zu neuem Wissen, zu neuen Ideen und Materialien verschmolzen. Zumindest aus Sicht des Surfers geht es im Gegensatz zu früheren Webanwendungen nun also um Inhalte und nicht mehr um technische Realisierung. Aus der Sicht des Programmierers werden heute jedoch deutlich komplexere Anforderungen an die Gestaltung und Programmierung einer Webseite gestellt. Reichte es bislang, eine Programmiersprache und bestenfalls ein wenig HTML zu beherrschen, ist dies inzwischen die minimale Grundanforderung, die durch Exploring JavaScript
81
3 – AJAX
gute CSS-Kenntnisse, flankiert von einem ausgeprägten Verständnis für DOM/ XML und JavaScript sowie durch einen guten Überblick über browsertypische Besonderheiten ergänzt werden muss. Berücksichtigt man, dass eine der für das Web 2.0 essentiell gewordenen Webtechniken – nämlich AJAX – auf genau diese Punkte aufsetzt, muss man also nicht nur serverseitig programmieren können, sondern auch in der Lage sein, bestimmte Teile einer Anwendung auf die Clientseite auszulagern und dementsprechend zu schreiben. Oder aus dem reinen JavaScript-Blickwinkel betrachtet: Im Gegensatz zu bisherigen ausschließlich im Browser laufenden Anwendungen, die sich je nach Geschwindigkeit des Computers des Users mehr oder minder performant zeigten, ist es eine überlegenswerte Alternative, Teile der Applikation auf einen Server auszulagern und dort ausführen zu lassen. Dies hat den Vorteil, dass der Umfang des zu ladenden JavaScript sinkt und Verarbeitungskapazität primär vom Server und nicht mehr vom Client bereitgestellt wird. Über diesen „Umweg“ ist eine effizientere Entwicklung browserbasierter Anwendungen möglich, da die Abhängigkeit von der vom Browser interpretierten JavaScript-Version sinkt.
3.1 AJAX Grundlagen Um diese Herangehensweise sinnvoll umsetzen zu können, wollen wir uns in diesem Abschnitt den konzeptionellen Grundlagen widmen, um darauf folgend die technische Realisierung anzugehen.
Achtung! Die gewählten Codebeispiele des AJAX-Kapitels zeigen normalerweise nur das grundlegende Vorgehen, ohne dabei Anspruch auf sichere Programmierung oder erhöhte Komplexität zu erheben. Es ist daher unumgänglich, bei einer eigenen Implementation saubere Inputvalidierung, Zeichenmaskierung etc. einzufügen, um die eigene Applikation gegen Angriffe zu wappnen! Weiterhin ist zu beachten, dass die gewählten Beispiele durchgängig auf UTF-8 als Defaultzeichensatz setzen. Änderungen im Charset können, so sie nicht konsequent umgesetzt werden, zu überraschenden Ergebnissen bei der Verarbeitung der verwendeten Daten durch JavaScript oder das Backend führen.
82
AJAX Grundlagen
3.1.1
Was genau ist AJAX?
Hinter dem Begriff AJAX verbirgt sich eine im Prinzip einfache, aber sehr effiziente und elegante Form, dynamisch Webcontent zu produzieren und an den User auszuliefern. „Asynchronous Javascript And Xml“, so die ausgeschriebene Fassung, erlaubt in seinem Konzept das sequenzielle Nachladen punktuell benötigter Informationen, ohne für jede Anfrage die gesamte Webseite neu vom Server abzurufen. Damit vermindert sich die Menge des übertragenen Contents, aktuelle Informationen werden schneller sichtbar und eine Webapplikation fühlt sich für den Benutzer fast wie eine lokal laufende Anwendung an. Ermöglicht wird dies durch die Verwendung des XMLHttpRequest-Objekts, das von JavaScript zur standardisierten Übertragung von Anfrage und Antwort verwendet wird. Dabei reagiert die Applikation selbst zum Beispiel auf Tastatureingaben, Mausklicks oder reines Überfahren von Seitenelementen mit dem Mauszeiger, die als auslösendes Moment für einen Request an das Backend betrachtet werden können. Im Gegensatz zum bekannten Klick auf einen Link oder einen Formular-Absendebutton zum Anfordern weiteren Contents kann bei einer AJAX-Anwendung jedes denkbare Element für verschiedenste Aktionen (Eingabe, Fokuserhalt, Fokusverlust, ...) einen JavaScript-Eventhandler besitzen, der je nach Programmierung auf das jeweilige Ereignis reagiert. So können zum Beispiel bei Bedarf abhängig von der vorangegangenen Auswahl des Benutzers zur Laufzeit komplette Inhalte von Dropdownfeldern generiert werden, anschaulich verdeutlicht durch GoogleSuggest.1
3.1.2
Initialisierung
AJAX als Begriff ist noch nicht sehr alt. Geprägt zirka 2005, beschreibt er aus technischer Sicht ein zum damaligen Zeitpunkt revolutionär wirkendes Ablaufschema einer Webapplikation: Durch das eingesetzte JavaScript sollten ausschließlich bei Bedarf Daten von einem Server nachgeladen werden, um sie in die vorhandene Seite zu integrieren, anstatt wie auf dem klassischen Wege die komplette Seite neu zu laden. Das dabei wichtigste Element sollte das XMLHttpRequest-Objekt sein, dessen Vorhandensein überhaupt erst ermöglichte, dass die notwendigen Serverrequests nicht mehr wie zuvor in synchroner Kommunikation ausgeführt würden, sondern in einer JavaScript-Anwendung asynchron abgearbeitet werden sollten. Damit nahm das XMLHttpRequest-Objekt die zen1. http://www.google.com/webhp?complete=1&hl=en
Exploring JavaScript
83
3 – AJAX
trale Stelle innerhalb des von James Garrett in seinem Aufsatz „Ajax: A New Approach to Web Applications“2 beschriebenen Konstrukts ein.
Microsoft und das XMLHttpRequest-Objekt Dieses bis dahin eher unscheinbar wirkende, von Microsoft eingeführte und mittels ActiveX-Komponente implementierte XMLHttpRequest-Objekt erlebte in der Folgezeit einen wahren Boom. Anwendungen wie Google Suggest zeigten das Potential asynchroner Kommunikation und führten förmlich zu einem Dammbruch, viele weitere Applikationen entstanden. Im Gegensatz zu Microsoft wurde die AJAX-Unterstützung von den anderen Browserherstellern nativ implementiert. Dies hatte den Vorteil, dass in allen anderen Browsern das XMLHttpRequest-Objekt immer als solches ansprechbar war und das massive Chaos der Entwicklung innerhalb der Internet-ExplorerFamilie keine weiteren Kreise zog. Tabelle 3.1 zeigt einen Überblick über die Verfügbarkeit respektive die Version zum Einführungszeitpunkt des XMLHttpRequest-Objekts in den jeweiligen Browsern. Browser
AJAX-Unter stützung ab
Nativ/AddOn
Anmerkung
Microsoft Internet Explorer
5.0
(ab 7.0)/ (ActiveXKomponente)
Alle auf dem IE basierenden Browser
Mozilla Firefox
1.0
?/-
Alle auf Gecko basierenden Browser ab Version 0.97 (November 2001)
Im Gegensatz zur nativen Implementierung aller anderen Browserhersteller bedeutet die bis zum Internet Explorer 7.0 vorherrschende Einbindung via ActiveXKomponente, dass sich die dazugehörige Schnittstelle immer wieder veränderte. So hieß das Objekt nicht einfach XMLHttpRequest, sondern zeigte sich stetig mit neuem Namen: Microsoft.XMLHTTP, MSXML2.XMLHTTP, MSXML2.XMLHTTP.3.0, MSXML2.XMLHTTP.4.0 oder MSXML2.XMLHTTP.5.0. Wer also eine AJAX-Applikation erstellen wollte, musste zwangsweise nicht nur eine Browserweiche programmieren, sondern innerhalb dieser auch die verschiedenen microsoftschen Namensgebungen abfangen. Eine entsprechende Lösung dafür zeigt das im Vorwort des Buches vorgestellte Snippet, das den Umgang mit dem Objekt vereinfacht (siehe Listing 0.1).
3.1.3
W3C
Für die Weiterentwicklung, Dokumentation und vor allem für die Vereinheitlichung der Schnittstelle des XMLHttpRequest-Objekts erarbeitete das W3C ein Working Draft3, das derzeit in der Version vom 27.09.2006 vorliegt. Darin werden die folgenden Methoden und Eigenschaften beschrieben:
Methoden 쐌 open(method, url, async, user, password) wird verwendet, um eine Verbindung zum Backend aufzubauen. Der Aufruf der Methode erfordert zwingend die Parameter method und url. Dabei enthält method die HTTP-RequestMethode (normalerweise GET oder POST) und url den URL der aufzurufenden Backendschnittstelle. Optionale Parameter sind async (true), user (null) und password (null). 쐌 setRequestHeader(header, value) kann verwendet werden, um zusätzliche Header mit dem Request zu übermitteln. Dabei müssen sowohl die Headerbezeichnung (header) als auch der dazugehörige Inhalt (value) angegeben werden. 쐌 send(data) übermittelt die angegebenen Daten an das Backend. 쐌 abort() bricht die Verbindung zum Backend ab, unabhängig davon, ob und wieviel Daten bereits übermittelt wurden. Alle Eigenschaften des Objekts werden auf ihren Initialwert zurückgesetzt. 쐌 getAllResponseHeaders() gibt alle übermittelten Header als String zurück. 쐌 getResponseHeader(header) enthält den Wert des zurückzugebenden Serverheaders. 3. http://www.w3.org/TR/XMLHttpRequest/
Exploring JavaScript
85
3 – AJAX
Eigenschaften 쐌 onreadystatechange ist die wichtigste Eigenschaft des XMLHttpRequest-Objekts. Es ermöglicht als EventListener erst die asynchrone Verarbeitung von Daten, da es aktiv wird, sobald sich der Verarbeitungsstatus des Requests verändert (readystatechange-Event). 쐌 readyState enthält den aktuellen Verarbeitungsstatus des Objekts. Das Attribut ist readonly und kann folgende Werte annehmen: 0 – Uninitialized: Grundwert bei Initialisierung 1 – Open: Die open()-Methode wurde erfolgreich aufgerufen 2 – Sent: Der Browser hat den Request erfolgreich versendet 3 – Receiving: Der Moment, bevor die Payload der Antwort übermittelt ist.
Zu diesem Zeitpunkt sind die HTTP-Header bereits übertragen worden. 4 – Loaded: Die Responsedaten sind vollständig eingetroffen
쐌 responseText enthält die vollständige, als Zeichenkette interpretierte Payload der Serverantwort. Readonly. 쐌 responseXML enthält die vollständige Payload der Serverantwort im XMLFormat. Readonly. 쐌 status enthält, sofern verfügbar, den HTTP-Statuscode (normalerweise 200, wenn die Verbindung erfolgreich verarbeitet wurde). Readonly. 쐌 statusText enthält, sofern verfügbar, den zum HTTP-Statuscode gehörenden Text, der vom Server gesendet wurde. Readonly. Der Working Draft enthält eine noch deutlich umfangreichere Beschreibung. Es zahlt sich daher aus, ihn bei der Entwicklung der ersten eigenen AJAX-Anwendung zur Hand zu haben.
Hinweis Auch wenn alle der oben erwähnten Browser die onreadystatechange()-Methode implementieren, so arbeitet diese leider nicht überall zuverlässig: Glücklicherweise steht jedoch bei allen zumindest der Wert 4 für den erfolgreichen Abschluss der Kommunikation mit dem Server zur Verfügung, so dass der entsprechende Handler darauf aufbauen kann.
86
AJAX Grundlagen
3.1.4
Synchrone oder asynchrone Kommunikation?
Bisher haben wir immer wieder den Begriff „synchrone“ beziehungsweise „asynchrone Kommunikation“ gelesen, doch worin unterscheiden sich beide Konzepte voneinander, und wie kommen sie zum Einsatz? Betrachten wir eine herkömmliche JavaScript-Anwendung, so stoßen wir bei umfangreicheren Funktionalitäten schnell auf das Problem, dass diese im Zweifelsfall den kompletten Browser lahmlegen, bis ihr Code vollständig abgearbeitet ist. Das bedeutet bei lokal verarbeiteten Webapplikationen, dass sie träge reagieren und der Benutzer bei Wartezeiten nicht einfach in einem anderen Browserfenster an etwas anderem arbeiten kann. Zudem können komplexe JavaScript-Programme schnell sehr speicherhungrig werden, was nicht nur den Browser ausbremst, sondern das gesamte System des Users. Hintergrund dessen ist, dass ein einmal abgesendeter Request erst zu Ende verarbeitet werden muss, bevor er die Kontrolle über seine „Umgebung“ wieder an den Benutzer abgibt. Das entsprechende Gegenstück existiert auch in Webapplikationen, die komplett servergestützt programmiert sind – auch hier wird erst eine vollständige Seite berechnet und danach ausgeliefert. Sind die Berechnungen komplex oder erfordert der Seitenaufbau externe Daten (z.B. aus einer Datenbank), die gerade nur schleppend angeliefert werden, erscheint dem User die Ladezeit einer Seite ungemein lang. Wir sprechen in beiden Fällen von synchroner Kommunikation – es muss jeweils erst ein Request vollständig verarbeitet sein, bevor der nächste Request ausgelöst wird. Dieses Prinzip kann bei lokalen Anwendungen teilweise durchaus sinnvoll sein – beispielsweise ist es in einer Textverarbeitung eher ungünstig, in einem Dokument zu schreiben, während es gerade gespeichert wird. Bei Webanwendungen haben wir jedoch häufig den Fall, dass ein Klick eigentlich nur einen meist kleinen Teil der aktuellen Webseite verändern soll. Hier die komplette HTML-Seite neu zu laden, ist zwar eine Möglichkeit, aber wenn man es genauer betrachtet, verschwendet man dadurch Rechenkapazität des Servers, Bandbreite und natürlich die Zeit des Surfers. Viel einfacher wäre es doch, wenn man im Hintergrund einfach nur den Bereich der Webseite austauschen könnte, der sich aufgrund der Aktionen des Benutzers verändert hat! Ein typisches Beispiel dafür ist die Implementation eines Fortschrittsbalkens für einen Dateiupload. Natürlich wirkt eine Webanwendung deutlich komfortabler, wenn sie ihren Benutzer darüber informiert, wieviel Prozent seiner Datei inzwischen zum Server übertragen wurden, während er selbst in einem anderen Browserfenster weiter surfen kann.
Exploring JavaScript
87
3 – AJAX
Aber woher bekommt unsere Webanwendung die entsprechenden Informationen? Und wie kann sie die angezeigte HTML-Seite auf dem Bildschirm dementsprechend ändern? Hier kommt die asynchrone Kommunikation ins Spiel. Statt den kompletten Browser permanent mit der Berechnung des aktuellen Upload-Status zu blockieren, wird die Ermittlung des Wertes dem Webserver selbst überlassen. Die Kommunikation zwischen Client und Server erstreckt sich daher auf eine knappe Anfrage und eine ebenso knappe Antwort. Beides ist schnell ausgetauscht und verarbeitet und steht dem Benutzer als Information ohne merkbare Verzögerung zur Verfügung. Für die Zeit zwischen den einzelnen Requests gibt der Browser die Kontrolle wieder an den User zurück, so dass der gesamte Vorgang wirkt, als würden die Daten „magisch“ im Browser entstehen. Auf diese Art werden unschöne Blockaden des Browsers vermieden. Zudem sinkt die Gefahr, dass aufgrund langer Skriptlaufzeit ein Warnfenster aufgeblendet wird, das dem Benutzer die Möglichkeit zum harten Abbruch des Prozesses offeriert.
3.1.5
GET oder POST?
Wie übermitteln wir nun die Daten an das Backend? Schicken wir sie besser per GET oder per POST? Diese typische Frage enthält fast schon die typische Antwort: Grundsätzlich empfiehlt es sich, Daten per POST zu versenden, gerade dann, wenn die Menge der zu übertragenden Daten die begrenzte Länge eines GET-Requests zu übersteigen in der Lage ist. Wir werden in den folgenden Beispielen sehen, dass aufgrund der in den meisten Fällen nicht im Vorfeld exakt abschätzbaren Datenmengen im Normalfall POST als HTTP-Request-Methode verwendet wird. Programmieren wir nur den JavaScript-Teil der Applikation, endet an dieser Stelle die notwendige Aufmerksamkeitsfokussierung. Auf der Serverseite müssen wir jedoch darauf achten, die hereinkommenden POSTDaten korrekt anzunehmen und mit passenden Methoden für die weitere Verarbeitung vorzubereiten. Verwenden wir im Backend beispielsweise PHP als Programmiersprache und sind die POST-Daten nicht als Formulardaten kodiert, dürfen wir die Daten nicht etwa über das superglobale $_POST-Array ansprechen, sondern müssen sie von php://input als vollständigen String einlesen und die Auswertung selbst vornehmen.
88
Formate
3.2 Formate Wir kennen damit die technischen Grundlagen, die zum Verständnis von AJAX notwendig sind, nun wollen wir uns den Umsetzungsdetails widmen. An erster Stelle sollen dabei die Übertragungsformate für die zu übermittelnden Daten stehen. Diese können wir in drei Hauptgruppen unterscheiden: Plaintext, XML und JSON. Jedes dieser Formate hat dabei seine eigenen Vor- und Nachteile, die es für den Einsatz mit AJAX mehr oder weniger gut eignen. Auch wenn das X in AJAX eigentlich für den XML-Anteil des Kommunikationswegs steht, bleibt es am Ende doch letztlich dem Programmierer überlassen, wie er die Übertragung von Daten gestaltet. Insofern ist keines der drei Hauptformate grundsätzlich „falsch“. Allerdings sollte vor der Entscheidung für ein bestimmtes Format eine gründliche Abwägung der gegebenen Anforderungen und notwendigen Verarbeitungsmöglichkeiten stehen.
3.2.1
Plaintext
Die simpelste Methode zur Datenübertragung ist sicherlich das Plaintextformat. Hier wird es im Prinzip nur notwendig, die erforderlichen Daten in einem String zusammenzuführen und diesen ohne Strukturierungsinformationen zwischen Client und Server zu transportieren. Doch diese Einfachheit ist gleichzeitig die größte Schwäche des Konzepts: Da jegliche Strukturinformationen fehlen, ist es schwierig, komplexe Datenstrukturen zu übertragen und auf der jeweiligen Gegenseite zu verarbeiten. Das Format eignet sich daher primär zur Gestaltung einfacher Requests, die nicht aufeinander aufbauen und deren Results ebenfalls möglichst skalare Daten ergeben. Die Listings 3.1 (Clientseite) und 3.2 (Serverseite) illustrieren diesen Sachverhalt: Plaintext <script type="text/javascript" src="../xhttp.js"> <script type="text/javascript"> //
Listing 3.1: Plaintext mit JavaScript
Exploring JavaScript
89
3 – AJAX var name=prompt('Verraten Sie uns Ihren Namen?'); if (!name) return; xhttp=getXmlHttp(); xhttp.onreadystatechange=_requestHandler; xhttp.open('GET','plain.php?name='+encodeURIComponent(name),true); xhttp.send(''); } function _requestHandler() { if (xhttp.readyState!=4) return; alert(xhttp.responseText); } //]]>
Betrachten wir einmal den Vorgang der Datenerhebung, -übermittlung und abschließenden -auslieferung genauer. Beim Aufruf unseres Skriptes erscheint eine normal anmutende Webseite, deren einziger Inhalt ein Link ist. Klicken wir diesen an, wird die im XHTML-Head notierte JavaScript-Funktion plainRequest() aufgerufen. Diese erfragt mittels eines aufgeblendeten Dialogfensters einen Namen. Ist ein Name eingegeben, wird auf der Basis der im Vorwort des Buchs vorgestellten grundlegenden getXmlHttp()-Funktion ein XMLHttpRequest-Objekt erzeugt. Diesem wird via onreadystatechange eine Requesthandlerfunktion zugeordnet, die für die asynchrone Verarbeitung der später vom Backend eintreffenden Antwort zuständig sein wird. Damit sind alle notwendigen Vorarbeiten zur eigentlichen Übermittlung der Daten an den Server abgeschlossen, und die Daten werden mittels der vom XMLHttpRequest-Objekt bereitgestellten Methoden open() und send() an das Backend gesendet. Dabei kommt die HTTP-Request-Methode GET zum Einsatz.
90
Formate
Dort passiert in unserem Fall nichts wirklich Spektakuläres mit den Daten, der übergebene Name wird in einen Begrüßungstext eingebettet und dieser komplett an den Browser zurückgeliefert (siehe Listing 3.2). Erst auf der Clientseite wird es nun wieder spannend: Sobald die erwartete Response geladen ist, wird der entsprechende Text aus xhttp.responseText ausgelesen und in einer Alertbox an den Benutzer ausgegeben. Natürlich kann man sich vorstellen, eine eigene Datenstruktur zu entwerfen, die es ermöglicht, nicht nur einfache skalare Werte zu übergeben, sondern auch komplexere Daten – allerdings muss man in diesem Fall ein grundlegendes Parsing entwerfen. Dass sich diese Arbeit nicht lohnt, zeigen die beiden folgenden Formate, die genau diesen Ansatz bereits von Hause aus implementiert haben.
3.2.2
XML/XML-RPC
XML ist das mit dem X in AJAX assoziierte Übertragungsformat. Im Gegensatz zur eben betrachteten Plaintextvariante können mittels XML vollständige Datenstrukturen übertragen werden, die von der Empfängerseite einfach ausgewertet werden können. So lassen sich komplette verschachtelte Datenbäume zu einem Request zusammenfassen, übermitteln und verarbeiten. Ein durchaus beachtenswerter Vorteil dieses Formates ist die breite Unterstützung durch viele gängige Programmiersprachen. Fast jede bietet die Möglichkeit, mit XML zu arbeiten und via XML-RPC zu kommunizieren. In vielen Fällen sind auch ausführliche Tutorien zur jeweiligen Sprachimplementierung vorhanden, so dass es der Programmierer leicht hat, sein eigenes Set an Funktionalität zu entwerfen. Der größte Nachteil von XML-RPC ist der Overhead, der durch die XML-konforme Auszeichnung der Daten sowie dessen Übertragung entsteht. Das bedeutet, dass sich letztlich auch die Zeit erhöht, die ein entsprechendes AJAX-Skript zu seiner Ausführung benötigt. Zudem müssen die Informationen der eigentlichen JavaScript-Objekte erst in XML übersetzt und entsprechend auch beim Response die XML-Daten in JavaScript-verständliche Strukturen übertragen werden. Betrachten wir die Listings 3.3 und 3.4, erhalten wir einen ersten Überblick über die Verwendung von XML-RPC zur Übergabe strukturierter Daten zwischen Client und Server:
Exploring JavaScript
91
3 – AJAX XML-RPC <script type="text/javascript" src="../xhttp.js"> <script type="text/javascript"> //\n'; xml += '<methodCall>\n'; xml += '<methodName>demoMethod\n'; if (params.length) { xml += '<params>\n'; for (var i = 0; i < params.length; i++) { xml += '<param><string>'+params[i]+ '\n'; } xml += '\n'; } xml += ''; document.getElementById('request').value=xml; xhttp=getXmlHttp(); xhttp.onreadystatechange=_requestHandler; xhttp.open('POST','xmlrpc.php',true); xhttp.send(xml); } function _requestHandler() { if (xhttp.readyState!=4) return; document.getElementById('response').value=xhttp.responseText; var div=document.getElementById('result'); div.innerHTML=''; var reverse=xhttp.responseXML.getElementsByTagName('string');
Vergleichen wir das Vorgehen, erkennen wir viele Bestandteile wieder, die wir bereits bei der Verarbeitung des Plaintextformats kennen gelernt haben. Das Skript selbst hat eine geringfügig andere Aufgabe, es liest einen oder mehrere Strings aus einem Dialogfenster und sendet die erfragten Zeichenketten gebündelt an das Backend. Dort wird die Reihenfolge der Zeichen umgekehrt und das komplette Set zurückgegeben. Auf der Clientseite wird diese Struktur nun wieder in ihre einzelnen Strings zerlegt und diese dem Benutzer angezeigt. Im Unterschied zur Plaintext-Variante müssen hier komplette Strukturen verwaltet und beidseitig (also auf dem Client und auf dem Server) geparst werden. Zusätzlich kommt die HTTP-Request-Methode POST zum Einsatz, da die Menge der zu übermittelnden Daten deutlich angestiegen ist. Daher erfolgt der Zugriff auf die Daten auf der Serverseite auch nicht mittels des superglobalen $_GET-Arrays, sondern wird als Stream von php://input gelesen und vom XML-RPC-Server umgewandelt und verarbeitet.
3.2.3
JSON/JSON-RPC
Das im Zusammenhang mit AJAX inzwischen meistverwendete Übertragungsformat ist allerdings JSON. Hier handelt es sich um eine Serialisierung des zu übermittelnden JavaScript-Objekts, bei der kaum zusätzliche Strukturierungsinformationen mitgegeben werden, so dass sich die letztlich zwischen Client und Server ausgetauschte Datenmenge im Vergleich zu einem XML-RPC deutlich vermindert. Hier liegt wohl neben der leichten Verarbeitbarkeit von JSONObjekten einer der Hauptvorzüge, die dieses Format für die meisten Entwickler von AJAX-Anwendungen so attraktiv macht. Sehen wir uns den praktischen Einsatz von JSON-RPCs einmal anhand eines Beispiels an: JSON-RPC <script type="text/javascript" src="../xhttp.js"> <script type="text/javascript" src="json.js"> <script type="text/javascript"> //
Listing 3.5: JSON mit JavaScript
94
Formate var request={ method: 'demoMethod', params: [], id: new Date().getTime() }; var x; while (x=prompt('Bitte einen String eingeben:')) { request.params.push(x); } if (request.params.length==0) return; xhttp=getXmlHttp(); xhttp.onreadystatechange=_requestHandler; xhttp.open('POST','server.php',true); var rpcstring=request.toJSONString(); document.getElementById('request').value=rpcstring; xhttp.send(rpcstring); } function _requestHandler() { if (xhttp.readyState!=4) return; document.getElementById('response').value=xhttp.responseText; var div=document.getElementById('result'); div.innerHTML=''; var response=xhttp.responseText.parseJSON(); for(var x=0; x\n'; } } //]]>
Die grundsätzliche Funktionalität entspricht dem XML/XML-RPC-Beispiel. Vergleicht man den JavaScript-Code beider Formate, sind die Ähnlichkeiten frappierend, jedoch zeigt die JSON-Variante ein deutlich entspannteres Handling. In Anbetracht der praktischen Wichtigkeit dieses Formats haben wir an dieser Stelle jedoch auf eine ausführliche Darstellung verzichtet und für JSON ein eigenes Kapitel reserviert.
3.2.4
Webservices mit SOAP
Natürlich liegt es nahe, statt auf eines der drei vorgestellten Formate auf einen auf SOAP basierenden Webservice zurückzugreifen. Grundsätzlich ist das mit AJAX durchaus möglich, denn auch mittels SOAP kann asynchrone Client-Server-Kommunikation realisiert werden. Im Gegensatz zu XML-RPC und JSONRPC ist SOAP jedoch weder leicht zu implementieren noch ressourcenschonend, wenn man die zur Kommunikation notwendige Datenmenge betrachtet. Dennoch wollen wir der Vollständigkeit halber einen Blick auf das zur Umsetzung benötigte Hintergrundwissen werfen.
96
Formate
Service Beschreibung mit WSDL Die Basis eines jeden sauber implementierten SOAP-Services ist ein WSDLFile, das die vorhandene API beschreibt und zum Validieren von Requests verwendet werden kann. Zwar funktioniert ein SOAP-Server auch ohne WSDL (Webservices Description Language), doch bedarf es ungleich mehr Augenmerk des Programmierers, konsistente Schnittstellen zu gewährleisten. Zusätzlich bedeutet es für Benutzer der SOAP-API, die gegen diese programmieren müssen, Extraaufwand, da sie nicht einfach auf eine für viele IDEs verständliche Implementationsbeschreibung zurückgreifen können, sondern sich auf die in der Praxis in vielen Fällen eher mangelhafte schriftliche Dokumentation verlassen müssen. Was macht nun ein WSDL-File so interessant, dass es für Anbieter eines SOAPWebservice zum guten Ton gehört? Als plattform-, programmiersprachen- und protokollunabhängige XML-Spezifikation stellt es eine Metasprache dar, in der sich die Funktionen, Daten und Datentypen beschreiben lassen, die ein Webservice anbietet und auf eine entsprechende Anfrage zurückliefert. Die Informationen lassen sich in mehrere Typen von Beschreibungselementen unterteilen, deren Elemente untereinander in Beziehung stehen können. Dabei werden jedoch lediglich die syntaktischen Bestandteile eines Webservice beschrieben, darüber hinausgehende (z.B. organisatorische) Informationen werden nicht zur Verfügung gestellt. Doch obwohl mit einem WSDL-File eine sehr detaillierte Schnittstellenbeschreibung vorliegt, die die Entwicklung eigener Clients sehr vereinfacht, hat sich SOAP im AJAX-Umfeld nicht durchsetzen können. Grund ist der protokollimmanente riesige Overhead zur Datenbeschreibung, der bei jedem Request und jedem Response übermittelt werden muss. Das kann dazu führen, dass der beschreibende Teil gut und gern das fünfzig- bis hundertfache der eigentlichen Payload ausmachen kann. Berücksichtigt man, dass das nicht nur verlängerte Übertragungszeiten, sondern auch erhöhten Aufwand für das Parsing der übermittelten Daten bedeutet, erkennt man den Konflikt: Da AJAX gerade deshalb als attraktive Ergänzung konventioneller Webseiten betrachtet wird, weil es blitzschnelle Kommunikation im Hintergrund ermöglicht, von der der Besucher der Webseite im Prinzip nichts mitbekommt, ist SOAP mit seiner eher für Massendatenübertragung ausgelegten Struktur für diesen Einsatzzweck eher ungeeignet.
Exploring JavaScript
97
3 – AJAX
Weiterführende Literatur zur SOAP-Implementierung in den Browsern bieten allerdings sowohl die Mozilla-Foundation4 als auch Microsoft5, 6 an.
3.3 DOM-Operationen Nachdem wir nun wissen, auf welche Art und Weise Daten zwischen Client und Server ausgetauscht werden, wenden wir uns der Verarbeitung der vom Server gelieferten Daten zu. Diese sollen ja im Normalfall passend zum jeweiligen Dokument aufbereitet, an der richtigen Stelle innerhalb der (X)HTML-Struktur eingehängt und für den User sichtbar werden. Der dabei wichtigste zu berücksichtigende Punkt ist, dass AJAX bzw. JavaScript auf das DOM (Document Object Model) des HTML-Dokuments zurückgreifen können und wir daher eine valide Dokumentstruktur benötigen, um problemfrei arbeiten zu können. Haben wir diese, müssen wir uns nur noch einige wenige grundlegende DOMOperationen einprägen, und schon können wir das Dokument nach unseren Vorstellungen umgestalten. Das Listing 3.7 beschränkt sich ausschließlich auf die Manipulation des Dokuments selbst, es sollte jedoch nicht schwer sein, das Vorgehen gedanklich mit den vorangegangenen Format-Beispielen zu kombinieren, um den vollen Umfang einer AJAX-Applikation zu erfassen: DOM Demo <script type="text/javascript">//
Listing 3.7: Arbeiten mit DOM-Operationen 4. http://www.mozilla.org/projects/webservices/ 5. http://msdn.microsoft.com/library/default.asp?url=/workshop/author/webservice/ webservice_node_entry.asp 6. http://msdn.microsoft.com/workshop/author/webservice/using.asp
98
DOM-Operationen img.setAttribute('alt','Google Logo'); document.getElementsByTagName('body')[0].appendChild(img); } function addHTML() { if (document.getElementById('htmldiv')) { alert('Bereits eingefügt!'); return; } var div=document.createElement('div'); div.setAttribute('id','htmldiv'); div.style.border='1px solid black;'; div.innerHTML='
Hier folgt ein kleines Beispiel
Löschen'; document.getElementById('body').appendChild(div); } function removeHTML() { var div=document.getElementById('htmldiv'); div.parentNode.removeChild(div); } //]]> [Bild einfügen] [HTML einfügen]
Listing 3.7: Arbeiten mit DOM-Operationen (Forts.)
Das Beispiel bündelt die typischen Aspekte der Arbeit mit DOM-Operationen: Es werden Texte und Bilder eingefügt und bei Bedarf verändert beziehungsweise wieder entfernt. Wir unterscheiden dabei die Basisvorgänge des Ermittelns eines bestimmten Elements, das Auslesen und Setzen von Textwerten und Attributen, das Erzeugen und Einfügen ganzer Nodes und dazugehörend auch das Löschen oder Ersetzen derselben.
Exploring JavaScript
99
3 – AJAX
3.3.1
Elemente finden
Der erste und wichtigste Schritt ist immer, das für die weitere Bearbeitung relevante Element innerhalb des DOM eines (X)HTML-Dokuments ausfindig zu machen. Dazu lassen sich verschiedene DOM-Methoden verwenden: 1. document.getElementsByTagName() 2. document.getElementById() getElementsByTagName() liefert eine Liste der passenden Elemente, über deren Elemente man iterieren kann. Dagegen gibt getElementById() exakt ein Element
zurück, da eine ID innerhalb eines (X)HTML-Dokuments nur einmal (und damit eindeutig) vergeben sein darf. (Der Internet Explorer hat im Übrigen die unangenehme Eigenart, bei der Verwendung von getElementById() auch Elemente über das name-Attribut zu finden, sofern kein ID-Wert gesetzt ist.) Zusätzlich kann man ein Element natürlich auch direkt ansprechen, wenn man seine genaue Stelle innerhalb des DOM kennt. Im obigen Beispiel wurden unter anderem diese Zeilen verwendet: document.getElementsByTagName('body')[0].appendChild(img); var div=document.getElementById('htmldiv'); div.parentNode.removeChild(div);
Listing 3.8: Elemente finden
3.3.2
Textwerte auslesen und setzen
Hat man das richtige Element gefunden, kann im nächsten Schritt beispielsweise der Inhalt der ermittelten Node ausgetauscht werden. Dieses Vorgehen wendet man an, wenn man nur eine Textpassage dem aktuellen Kontext entsprechend austauschen will. Das könnten in der Realität zum Beispiel die Anzeige des momentanen Inhalts eines Warenkorbs, eine aktualisierte Serveruhrzeit oder eine entsprechend der getätigten Usereingaben angepasste Darstellung von Profildaten sein.
In unserem Beispiel haben wir dafür das innerHTML-Attribut des betreffenden Elements verwendet: div.innerHTML='
Darin wird der jeweilige Inhalt gespeichert, und dynamische Änderungen werden bei der Aktualisierung des Elementinhaltes interpretiert. Aus technischer Sicht bietet es sich an, auf diese Eigenschaft erst bei der Veränderung des Dokuments und nicht schon bei dessen Generierung zuzugreifen, da dies in einigen Fällen zu Verarbeitungsproblemen und unerwünschten Fehlermeldungen führen kann.
3.3.3
Attribute auslesen und setzen
Eine weitere Möglichkeit zur Veränderung des DOM des angezeigten Dokuments besteht in der Veränderung von Attributen eines oder mehrerer (X)HTMLTags. Dies kann zum Beispiel dann sinnvoll sein, wenn man on the fly Bereiche der Webseite ein- oder ausblenden oder auch Style-Informationen verändern will. Dafür stehen uns die beiden folgenden Wege zur Verfügung: 쐌 setAttribute() 쐌 Direktzugriff Während wir mit dem Direktzugriff nur bereits vorhandene Attribute verändern können, gibt die setAttribute()-Methode die zusätzliche Möglichkeit, einem Element ein neues Attribut hinzuzufügen. In Listing 3.10 finden wir unter anderem diese Anwendungen: img.src='http://www.google.com/images/logo_sm.gif'; img.setAttribute('alt','Google Logo'); div.setAttribute('id','htmldiv'); div.style.border='1px solid black;';
Listing 3.10: Attribute auslesen und setzen
3.3.4
Nodes erzeugen und einfügen
In manchen Fällen ist es allerdings nicht damit getan, bereits vorhandene Nodes zu verändern, sondern wir wollen dem Benutzer permanent neue Nodes anzeigen, ohne dabei den bereits vorhandenen Content zu verwerfen. In diesem Fall müssen wir also wissen, vor oder hinter welche bereits vorhandene Node wir einen weiteren Knoten in das Dokument einfügen wollen. Haben wir diese Information, können wir eine der drei folgenden Methoden anwenden: 쐌 createElement() 쐌 appendChild() 쐌 insertBefore() Exploring JavaScript
101
3 – AJAX
Dabei benötigen wir die Methode createElement(), um einen neuen Knoten zu erstellen. Mit appendChild() fügen wir diesen Knoten als letztes Element des angegebenen Elternelements ein – insertBefore() fügt die Node ebenfalls ein, jedoch vor dem angegebenen Element. In unserem Einstiegsbeispiel finden wir entsprechenden Code: var img=document.createElement('img'); document.getElementsByTagName('body')[0].appendChild(img);
Listing 3.11: Nodes erzeugen und einfügen
3.3.5
Nodes entfernen oder ersetzen
Das Gegenstück zum gerade gezeigten Erzeugen und Einfügen von Dokumentknoten ist das Entfernen oder komplette Ersetzen von Nodes innerhalb des DOM. Dafür verwenden wir die Methode 쐌 removeChild(). In der Anwendung kann dabei Code wie dieser entstehen: div.parentNode.removeChild(div);
Listing 3.12: Nodes entfernen
Zusammenfassend lässt sich festhalten, dass es mittels JavaScript sehr einfach ist, Veränderungen im DOM eines Dokuments vorzunehmen und diese dem Benutzer anzuzeigen. Die dafür benötigten grundlegenden Methoden und Eigenschaften der JavaScript-Objekte sind schnell erlernt und einsatzbereit – und sie funktionieren in den aktuellen Browsern relativ zuverlässig. Dies machen sich AJAX-Frameworks wie Dojo7 zunutze, die ein umfangreiches Angebot bereits implementierter Funktionaliäten bereitstellen, welche die Entwicklung eigener AJAX-Anwendungen auf der JavaScript-Seite deutlich erleichtern und effizienter werden lassen.
7. http://www.dojotoolkit.org/
102
XPath
3.4 XPath Sehr mächtig in seinen Möglichkeiten der zielgerichteten Navigation in DOMBäumen ist XPath. Dieses vom W3C formulierte Werkzeug gestattet mittels einer übersichtlichen Menge an Ausdrücken, aus XML-Strukturen die gewünschten Informationen zu selektieren. Bedauerlicherweise unterstützt jedoch nur die Mozillafamilie die native Anwendung von XPath mit JavaScript, so dass es eher als Mittel der Wahl für reine mozillabasierte Anwendungen gilt. Sobald jedoch browserübergreifende Webapplikationen erstellt werden sollen, muss der Entwickler auf XPath verzichten oder beim IE auf ActiveX-Komponenten zurückgreifen. Wir haben daher schweren Herzens auf eine eingehende Darstellung verzichtet, auch wenn es durchaus Anwendungsfälle wie ein firmeninternes Intranet geben mag, in denen keine Rücksicht auf nicht zur Mozillafamilie gehörende Browser genommen werden muss. Andererseits stellt das W3C eine sehr eingängige Beschreibung von XPath8 bereit, die zusammen mit den oben aufgeführten DOMOperationen schnell zu einem produktiven Entwicklungserfolg führen sollte.
3.5 Zusammenfassung Auch wenn man den Hype selbst nicht mag, ist AJAX eine spannende Entwicklung und der logisch nächste Schritt zur besseren Benutzbarkeit von Webanwendungen. Schnelle, flexibel reagierende Systeme, geringere Wartezeiten für das Rendern der veränderlichen Seitenbestandteile, pro Veränderung weniger Traffic, relativ einfache Implementierung; bei all den Pluspunkten fragt man sich natürlich, warum dann noch nicht alle Webseiten auf AJAX-Technologien setzen? Die Antwort ist ebenso einfach wie im ersten Augenblick verblüffend: Weil die Betreiber ein gutes Ranking in den Suchmaschinen erwarten und weil der Einsatz von AJAX sehr viel Sorgfalt erfordert, will man sich nicht zusätzliche Sicherheitsprobleme ins Haus holen. Zudem ist nicht in jedem Browser JavaScript aktiviert, sei es aus persönlicher Präferenz des Anwenders, sei es, weil der Browser kein JavaScript interpretiert oder weil der Benutzer aufgrund seiner Position zum Beispiel in einem Firmennetzwerk aufgrund der internen Sicherheitsrichtlinien eben kein JavaScript zur Verfügung hat. Wir wollen an dieser Stelle kurz auf die Probleme eingehen, die sich aus dem Einsatz von AJAX ergeben können. 8. http://www.w3schools.com/xpath/default.asp
Exploring JavaScript
103
3 – AJAX
Der häufig an erster Stelle genannte Punkt ist die Auffindbarkeit in Suchmaschinen. Wenn wir uns in Erinnerung rufen, was ein Crawler einer Suchmaschine tut9, dass er nämlich JavaScript im Normalfall nicht auswertet, erkennen wir, dass eine Seite, die komplett auf AJAX aufsetzt, einer Suchmaschine nicht viel sinnvoll verwertbaren Content anbietet. Denn alle nachgeladenen Informationen sind für den Crawler unerreichbar und werden daher nicht indexiert. Dies kann man natürlich auch bewusst einsetzen, eben wenn man nicht möchte, dass bestimmte Bereiche einer Seite indexiert werden. Der zweite Punkt, der erforderliche Aufwand für eine sichere Implementierung, wird deutlich seltener genannt. In vielen Fällen ist leider weder den Programmierern noch den Betreibern einer Seite klar, wie verwundbar gerade JavaScript-Applikationen für Angriffe von außen sind. Führt man sich jedoch vor Augen, wie viel Kontrolle eine JavaScript-Applikation über den Browser und im Zweifelsfall über den kompletten Rechner des Benutzers hat, erahnt man die mögliche Tragweite eines Angriffs. Es ist jedoch erfreulich, dass zunehmend mehr Programmierer am Thema Sicherheit interessiert sind, wie steigende Besucherzahlen entsprechender Vorträge auf Kongressen und diverse qualifizierte veröffentlichte Artikel zeigen. Während man auf die beiden ersten Punkte selbst Einfluss nehmen und über ihre Berücksichtigung entscheiden kann, ist der dritte Aspekt außerhalb des Zugriffsbereichs eines Webseitenbetreibers. Nicht selten ist in Unternehmen und Behörden an den Arbeitsplätzen der Mitarbeiter kein JavaScript aktiviert, so dass man im Zweifelsfall Kunden, statt sie mit der eigenen Site zum Kauf zu bewegen, verärgert, weil für sie die Seite durch den massiven Einsatz von JavaScript nicht mehr benutzbar ist. Dies im Hinterkopf habend, können wir uns recht entspannt an AJAX-basierte Projekte machen, denn als Feature einer Webseite sind sie sehr wohl ein nicht zu unterschätzender Pluspunkt für viele User. Denken wir, um den Bogen zurück zum Anfang zu schlagen, an Google Suggest: Die Anzeige möglicher Treffermengen zum Suchbegriff und zu verwandten Suchtermen ist ein hervorragendes Beispiel für punktgenauen Einsatz. Die Webseite funktioniert im Zweifelsfall auch ohne dieses Feature, aber wenn es der Besucher nutzen kann, ergibt sich daraus für ihn ein angenehmer Mehrwert.
Von Tobias Struckmeier Zur Zeit, als der Begriff „AJAX“ maßgeblich von Jesse James Garrett (Adaptive Path), geprägt wurde, sah man XML als „das“ Format für neuartige Webanwendungen, die eine hohe Geschwindigkeit und komfortable Bedienung versprachen. XML ist weit verbreitet, in vielen Sprachen relativ einfach zu verarbeiten und ein wahres Allroundtalent. Gleichgültig welche Daten gespeichert werden sollen, XML kann es leisten. Diese Vielseitigkeit hat allerdings auch Nachteile. Es handelt sich um eine sehr ausschweifende Beschreibungssprache, die vielfältigsten Anforderungen gerecht werden soll – dabei allerdings auch ein entsprechendes Datenvolumen erzeugt und zuerst durch einen Parser prozessiert werden muss, bevor auf die Daten zugegriffen werden kann. Natürlich gibt es aber in fast allen Programmiersprachen entsprechende Erweiterungen, die dem Entwickler diese Arbeit abnehmen. Seit einiger Zeit existiert speziell im Zusammenhang mit JavaScript noch ein konkurrierendes Format – JSON (JavaScript Object Notation). Grundsätzlich ist JSON nichts anderes als serialisierte JavaScript-Objekte und -Arrays. Genauer betrachtet erkennt man, dass es sich hierbei um JavaScript-Quellcode (in Kurzschreibweise) handelt, wie er bereits in der JavaScript-Spezifikation von 1999 definiert wurde. Dies wird deutlich, wenn man versucht, die von einem Server abgefragten und im JSON-Format ausgelieferten Daten direkt in JavaScript zu evaluieren. Hierbei erhält man sofort vollwertige JavaScript-Objekte und -Arrays. Wie bei XML ist es also auch hier möglich, Hierarchien abzubilden und Objekte zu verschachteln. Das Akronym JSON und das dahinterstehende Verfahren wurde von Douglas Crockford ersonnen – einem prominenten Verfechter von JavaScript und Yahoo!-Mitarbeiter. AJAX-Anwendungen profitieren vom schlanken JSON-Format, da es einfach und ressourcenschonend verarbeitet werden kann sowie weniger Bandbreite und Traffic als XML benötigt. Erstaunlicherweise büßt es dabei gegenüber XML kaum an Lesbarkeit ein. Kurzum ein Format, das für JavaScript und AJAXAnwendungen prädestiniert ist.
Exploring JavaScript
105
4 – JSON
4.1 Das Format Gut sichtbar wird die Sparsamkeit von JSON, wenn man es einem XML-Dokument gegenüberstellt. Vergleichen wir also zunächst einmal ein XML-Dokument mit dem JSON-Format. Bei beiden Beispielen handelt es sich um die gleiche Menge Daten. Das XML-Dokument könnte folgendermaßen aussehen: <article> 0001Orangen - Navellinas <price>1,99 500 <article> 0002Birnen – Harrow Sweet <price>2,50 1000 <article> 0003Bananen - Zwergbananen <price>2,99 300
JSON benötigt hier nicht einmal die Hälfte der Zeichen, die für die gleichen Daten in XML benötigt werden. Lesbar sind die Daten jedoch weiterhin erstaunlich gut, was gerade beim Debuggen von Fehlern zweifellos eine willkommene Erleichterung darstellt. Dem Motto „Know your tools“ folgend, wollen wir uns das Format einmal genauer ansehen. Die Syntax von JSON ist relativ simpel. JSON kennt grundsätzlich zwei Datenstrukturen, Objekte und Arrays. Objekte werden mit geschweiften Klammern „{}“ umschlossen – Arrays mit eckigen „[]“. 쐌 Objekte bestehen aus einer Ansammlung von kommaseparierten Eigenschaften. Jede Eigenschaft muss aus dem Namen (String) sowie aus dem zugehörigen Wert (Value) bestehen. 쐌 Arrays bestehen ausschließlich aus einzelnen, mit Kommata getrennten Werten (Values), daher sind ausschließlich indizierte Arrays verwendbar. Der Grund liegt in den Schwächen von JavaScript bei der Verwaltung assoziativer Arrays. Bei Bedarf sollten also Objekte für die Datenhaltung immer Arrays vorgezogen werden. Die Zugriffsweise auf Arrays und Objekte innerhalb von JavaScript unterscheidet sich dabei nicht.
Exploring JavaScript
107
4 – JSON
Beispiel für ein Objekt {"Name":"Max", "Nachname":"Muster"}
Listing 4.3: Ein Objekt in JSON
Definiert wird hier ein Objekt mit zwei Eigenschaften. An erster Stelle steht die Eigenschaft Name mit dem Wert „Max“, gefolgt von Nachname mit dem Wert „Muster“.
Beispiel für ein Array ["Array", "mit", 4, "Werten"]
Listing 4.4: Ein Array in JSON
Das Array ist wesentlich simpler, es besteht nur aus einer Auflistung von Werten. Die unterstützten Datentypen der einzelnen Werte, die in Arrays und Objekten genutzt werden können, sind String, Number (Zahl), Boolean (true/false), null (Kein Inhalt) oder aber wiederum ein Array oder Objekt. Durch die beiden zuletzt genannten zulässigen Datentypen ist es möglich, Arrays und Objekte beliebig zu verschachteln, um komplexere Datenstrukturen abzubilden. Zur Veranschaulichung folgt ein Array, bestehend aus zwei Personen. Jede Person wird durch ein Objekt symbolisiert, das wiederum ein weiteres Objekt mit der Auflistung der Telefonnummern enthält: [{ "Vorname": "Max", "Nachname": "Muster", "Telefonnummern": { "Privat": "+001-555-345 345", "Mobil": "+001-555-444 444" } },{ "Vorname": "Lisa", "Nachname": "Lustig",
Listing 4.5: Kontaktliste in JSON
108
Das Format "Telefonnummern": { "Privat": "+001-555-111 765", "Mobil": "+001-555-222 991" } }]
Listing 4.5: Kontaktliste in JSON (Forts.)
Der Datentyp String kann aus beliebigen Zeichen bestehen. Die Zeichen ", \ und / sowie die Steuerzeichen Backspace \b, Formfeed \f, Newline \n, Carriage return \r und Tabulator \t müssen mit einem einleitenden Backslash maskiert werden, weil sie bereits ein spezielle Bedeutung in der JavaScript-Syntax haben. Unicodezeichen werden gleichfalls maskiert durch ein führendes \u und die vierstellige Hexadezimalrepräsentation des entsprechenden Zeichens. Ein Ä entspräche damit der UTF-8-Kodierung \u00C4, womit wir unserem ersten Beispiel auch Äpfel hinzufügen könnten. { "article": { "id": 4, "name": "\u00c4pfel - Granny smith", "price": 2.99, "quantity": 3000 } }
Listing 4.6: JSON-String mit UTF-8-Zeichen
Der Datentyp Number kann beliebige positive und negative Zahlen sowie Fließkommazahlen aufnehmen. Ferner kann man sich auch der Exponentialschreibweise (beispielsweise 1.56E+02) bedienen. Wie man sieht, ist die Syntax nicht sonderlich kompliziert. Mit den hier erklärten Details sollte man in der Lage sein, auftretende Fehler bei der Verarbeitung der Daten aufzuspüren. Auch wenn dies aufgrund der vielen erprobten Implementierungen in den seltensten Fällen nötig sein wird.
Exploring JavaScript
109
4 – JSON
4.2 JSON mit JavaScript Wie bereits am Anfang dieses Kapitels angedeutet, handelt es sich bei JSON um JavaScript Quellcode. Um die Herkunft ein wenig zu illustrieren, verfolgen wir hier erst den simpelsten Weg – die direkte Ausführung des JSON-Strings.
Wichtiger Hinweis Da es sich hier um Quellcode handelt, ist die Benutzung von eval() in echten Anwendungen zu gefährlich. Ein möglicher Angreifer könnte durch Programmierfehler in der Anwendung eigenen JavaScript-Code einschleusen, um die Benutzer oder deren Rechner zu schädigen! Der berühmte MySpace-Wurm zeigte, welch kritische Folgen eingeschleustes JavaScript haben kann. Der Wurm nutzte eine Sicherheitslücke in der MySpaceSeite, um beim Aufruf eines manipulierten URL eigenen JavaScript-Code im Browser des Besuchers auszuführen. Geschickt wurde der Browser mit JavaScript dazu gebracht, seinerseits mit dem Account des Besuchers weitere manipulierte URLs zu produzieren, und diese im System zu verteilen. Da es sich bei MySpace um eine stark frequentierte Plattform handelt, wurde der Wurm schnell zu einer wahren Epidemie, die bei den Betreibern sicher einige Kopfschmerzen verursachte. Wandeln wir also einen JSON-String in ein JavaScript-Objekt beziehungsweise in ein Array um. Dazu lassen wir diesen String direkt durch JavaScript interpretieren. var json= '[{"Vorname":"Max", "Nachname":"Muster", "Telefonnummern": {"Privat": "+001-555-345345", "Mobil": "+001-555-444444"}}, {"Vorname": "Lisa", "Nachname":"Lustig", "Telefonnummern": {"Privat": "+001-555-111765", "Mobil": "+001-555-222991"}}]'; var contactlist=eval('('+json+')'); // JSON String parsen
Listing 4.7: Evaluieren eines JSON-String
Die Funktion eval() führt den an die Funktion übergebenen String als JavaScriptCode aus und liefert das Ergebnis (in diesem Fall ein Array mit Objekten) zurück. Die runden Klammern um den JSON-String zeigen dem JavaScriptInterpreter an, dass wir nicht das Ergebnis eines Ausdrucks, sondern ein Objekt zurückerhalten möchten. Damit sind die Daten bereits in JavaScript verfügbar. Die Variable contactlist ist ein Array, das unsere Kontaktobjekte enthält. 110
JSON mit JavaScript
Der Zugriff mit JavaScript ist nun ein Leichtes: Beispiel Kontaktliste <script type="text/javascript" language="javascript"> var json='[{"Vorname":"Max","Nachname":"Muster","Telefonnummern": {"Privat":"+001-555-345345","Mobil":"+001-555-444444"}}, {"Vorname":"Lisa","Nachname":"Lustig","Telefonnummern": {"Privat":"+001-555-111765","Mobil":"+001-555-222991"}}]'; var conctactlist=eval('('+json+')'); document.write('
Listing 4.8: Dekodierung und Ausgabe der Kontaktliste
In einer realen Anwendung würden die JSON-Daten natürlich von einem Server geholt und nicht, wie hier im Beispiel, aus einem zuvor präparierten String (im Kapitel zum Thema AJAX genauer erläutert). Wenn ein vom Server übermittelter String direkt mit einem Interpreter ausgeführt wird, muss die Anwendung der Serverseite ein erhebliches Vertrauen entgegenbringen. Es ist möglich, dass hierdurch Schadcode auf dem Clientrechner ausgeführt wird – ob böswillig durch den Entwickler des JSON-Dienstes oder Exploring JavaScript
111
4 – JSON
durch eine Sicherheitslücke. Auch wird durch eval() keinerlei Validierung des übergebenen Codes durchgeführt. Dies bedeutet, dass Fehler im JSON-String, sei es durch einen Programmier- oder einen Übertragungsfehler, unsere Applikation im Ablauf stören könnten. So ist eine ordentliche Fehlerbehandlung erheblich erschwert. Aus all diesen Gründen ist daher vom Einsatz von eval() dringend abzuraten. Die Alternative ist, einen eigenen Parser für JSON in JavaScript zu implementieren. In den meisten Fällen kann man sich diese Arbeit allerdings sparen, da bereits fertige Parser zur freien Verfügung stehen1, deren Einbindung und Verwendung einfacher ist als der sauber unter Sicherheitsaspekten ausprogrammierte Einsatz von eval(). Der hier verwendete Parser von www.json.org erweitert den Datentyp String um die Funktion parseJSON(), wodurch wir unser Script nur noch auf folgende Weise modifizieren müssen, um es den geänderten Anforderungen anzupassen: Beispiel Kontaktliste <script type="text/javascript" language="javascript" src="json.js"> <script type="text/javascript" language="javascript"> var json='[{"Vorname":"Max","Nachname":"Muster","Telefonnummern": {"Privat":"+001-555-345345","Mobil":"+001-555-444444"}}, {"Vorname":"Lisa","Nachname":"Lustig","Telefonnummern": {"Privat":"+001-555-111765","Mobil":"+001-555-222991"}}]'; var kontaktliste=json.parseJSON(); // JSON String parsen // Kontaktliste ausgeben document.write('
Listing 4.9: Dekodierung und Ausgabe der Kontaktliste mittels JSON-Parser 1. http://www.json.org/json.js
112
Verbreitung von JSON }
Listing 4.9: Dekodierung und Ausgabe der Kontaktliste mittels JSON-Parser (Forts.)
Nötig war nur die Ersetzung von eval() durch json.parseJSON(). Da nun kein externer JavaScript-Code mehr ausgeführt wird, erreichen wir damit jedoch eine erheblich höhere Sicherheit. Weiterhin können wir entsprechende Fehlerbehandlungen einbauen, um im Fehlerfall angemessen zu reagieren. Das Parsen mit JavaScript anstatt mit eval() hat nur marginale Geschwindigkeitseinbußen zur Folge. Überdies gewinnen wir mit dem JSON-Parser auch weitere Funktionalität. Die Datentypen Array, Boolean, Date, Number, Object und String verfügen über eine neue Funktion toJSONString(). Diese Funktion serialisiert die Daten zu einem JSON-String und erleichtert es somit, bei der Kommunikation mit einem Server ebenfalls JSON-Daten zu senden.
Hinweis Der in unserem Beispiel verwendete JSON-Parser lag zur Drucklegung in der Version vom 06.12.2006 vor. Leider wurde die API in der Vergangenheit bereits mehrmals modifiziert, daher kann an dieser Stelle nicht gewährleistet werden, dass dieses Beispiel auch in naher Zukunft unverändert funktioniert.
4.3 Verbreitung von JSON JSON ist bereits stark verbreitet, auch abseits von JavaScript. PHP wird seit Version 5.2 standardmäßig mit der JSON-Extension ausgeliefert. Sie ermöglicht die Konvertierung von PHP-Objekten und -Arrays in das JSON-Format und umgekehrt. Auf der PHP-Developermailingliste wurden bereits Diskussionen über die Einführung einer superglobalen Variable ($_JSON) zum Zugriff auf JSONDaten geführt.
Exploring JavaScript
113
4 – JSON
Viele weitere Sprachen unterstützen das JSON-Format ebenfalls. Python beispielsweise hat die Möglichkeit, JSON direkt zu evaluieren, da es wie bei JavaScript auch Teil der Pythonsyntax ist. Hier gilt natürlich ebenfalls, im Hinblick auf Sicherheit einen der zahlreichen freien Parser zu nutzen. Das Microsoft AJAX Framework bietet die entsprechende Basis für .Net, wovon Microsoft in seinen AJAX-Anwendungen kräftig Gebrauch macht. Java- und Rubyentwicklern stehen gleichermaßen JSON-Implementationen zur Verfügung. In nahezu jeder anderen halbwegs populären Programmiersprache existiert ebenfalls eine Unterstützung für JSON. Durch diesen hohen Verbreitungsgrad wird JSON zunehmend auch als trafficsparendes Format für den Datenaustausch und RPCs (Remote Procedure Calls) zwischen beliebigen Anwendungen interessant. Sicher ist es der breiten Unterstützung und einfachen Nutzbarkeit zu verdanken, dass viele neue Dienste der Web 2.0-Welle eine JSON-API bereitstellen. Beispielsweise Google mit seinen Such-, Kalender- oder Kartendiensten, Yahoo mit Flickr und del.icio.us oder OpenBC mit Xing – JSON ist fast überall zu finden. Ein Indikator für eine vielversprechende Zukunft.
4.4 Wie geht es weiter? Zukünftig wird die Verbreitung sicher weiterhin zunehmen. Für Web 2.0Dienste ist es besonders wichtig, eine leicht nutzbare API anzubieten. Dies erleichtert den Entwicklern die Interaktion mit dem Dienst, wodurch die meisten Web 2.0-Dienste erst richtig interessant werden. JSON ist hierfür das ideale Werkzeug. Die direkte Unterstützung durch Programmiersprachen und die Verbreitung bei populären Webdiensten werden JSON in Zukunft weiter nach vorn treiben. Mit JSON-RPC2 bemüht sich eine auf JSON aufsetzende Spezifikation, RPC mittels JSON zu vereinheitlichen. Da JSON allein nicht definiert, wie angefragte Daten strukturiert sein müssen, steht es jedem Entwickler frei, eine beliebige Hierarchie aufzubauen; entsprechend vielfältige Varianten existieren. Eine Applikation müsste dann auf jeden verwendeten Dienst extra angepasst werden. Hier setzt JSON-RPC an. Es stützt sich auf POST-Anfragen mit einer definierten Struktur. Diese Struktur erfordert ein Objekt mit drei Eigenschaften: die aufzurufende Methode method, die Parameter als Array params und, um Anfrage und 2. http://json-rpc.org/
114
Wie geht es weiter?
Antwort einander zuordnen zu können, eine ID. Die Antwort des Servers muss ebenfalls drei Eigenschaften enthalten. Die ID, auf die sich die Antwort bezieht, ein Rückgabeobjekt result und error für die Beschreibung aufgetretener Fehler. Ein Beispiel, um die Methode getInventoryItem mit der ID des zu holenden Objektes aufzurufen, würde so formuliert werden: { "method":"getInventoryItem", "params":[3],"id": 42}
Listing 4.10: JSON-RPC Anfrage
Die dazu passende Antwort: { "result":"{"article":{"id":3,"name": "Bananen - Zwergbananen","price": 2.99,"quantity": 300}", "error":null, "id":42 }
Listing 4.11: JSON-RPC Antwort
Es existiert sogar ein Vorschlag für ein JSON-Request-Objekt, das es in Anlehnung an XMLHttpRequest ermöglichen soll, innerhalb des Browsers Anfragen an einen beliebigen Server zu senden. Das JSON-Request-Objekt soll dabei das JavaScript-Objekt automatisch serialisieren. Ein schwieriges Thema ist dabei die gewünschte Möglichkeit, die Cross-Domain-Restriktion der Browser zu umgehen, um flexibler bei der Wahl der Gegenstelle zu sein. Dies wirft natürlich Sicherheitsfragen auf, denn es würde ja auch jedem Angreifer erleichtert, über Domaingrenzen hinweg Daten im Hintergrund zu senden. Der aktuelle Entwurf möchte diesem Missstand mit mehreren Mitteln begegnen. Zum einen soll das JSON-Request-Objekt einen eigenen Content-Type (application/jsonrequest) fordern und sicherstellen, dass die ausgetauschten Daten ausschließlich JSON-kodiert übertragen werden. Zum anderen sollen der Inhalt des aktuellen Dokuments vor der Gegenstelle abgeschirmt werden und der aktuelle Browserkontext (HTTP-Authorisation, Cookies) für den angefragten Server nicht zugreifbar sein. Es bleibt spannend, ob sich dieser Entwurf durchsetzen kann. Eines ist jedoch sicher, mit oder ohne JSON-Request-Objekt – JSON wird seinen bisherigen Siegeszug weiter fortsetzen.
Exploring JavaScript
115
5
Zeit und Datum in JavaScript
Von Chuck Easttom
5.1 Das Date-Objekt JavaScript bringt ein Objekt namens Date mit sich, mit dem man einfach zeitund datumsbezogene Aufgaben erledigen kann. Zeit und Datum werden beide vom Objekt Date abgeleitet, das eine große Anzahl an Methoden im Zusammenhang mit Zeit und Datum beinhaltet. Das DateObjekt verhält sich so wie jedes andere Objekt. Wenn Sie das aktuelle Datum in Ihrem Skript verwenden wollen, müssen Sie zuerst eine neue Instanz des DatumObjekts erstellen. Sie können dann die gewünschten Daten von dieser Instanz beziehen. JavaScript behandelt Daten wie Java. Viele Methoden wurden in beide Sprachen implementiert, wodurch ein sichtbarer Parallelismus entsteht. Alle Daten werden als Zahl der Millisekunden seit dem 1. Januar, 1970, 00:00:00 Uhr gespeichert.
5.2 Erstellen einer Date-Instanz Das Date-Objekt ist ein einzelnes internes Objekt, mit dem man Instanzen erstellen kann, um codierte Daten zu speichern, bezogen auf das Datum und die Zeit eines bestimmten Moments. Das Date-Objekt ist ein internes Objekt, genauso wie das Array-Objekt, das wir im vorherigen Kapitel besprochen haben. Es funktioniert als eine Vorlage, wenn man Instanzen von diesem Objekt erstellt. Die einfachste Anweisung im Zusammenhang mit dem Date-Objekt ist natürlich diejenige, mit der man eine Instanz mit den vorgegebenen Argumenten erstellt: var dateInstance = new Date()
Die Anweisung weist der Datenstruktur dateInstance eine Instanz des Objekts Date zu. Schauen Sie sich folgendes Skript einmal an: var now = new Date() alert(now)
Exploring JavaScript
117
5 – Zeit und Datum in JavaScript
Die Ausgabe dieses Skripts wird in Abbildung 5.1 gezeigt (das Datum und die Zeit sind natürlich entsprechend Ihrem momentanen Datum):
Abbildung 5.1: Eine Alertbox gibt das das aktuelle Datum sowie die aktuelle Zeit aus
Obwohl now ein Bezeichner für eine Instanz des Date-Objekts ist, enthält es auf seinem obersten Level eigentlich einen String. Wenn man den Wert von now ausgibt, scheint es ein String zu sein. Da es aber nicht wirklich zu dem Objekt String gehört, gelten die üblichen String-Methoden und Eigenschaften nicht für now. Das folgende Skript erzeugt daher einen Fehler, weil es die Methode split() nur bei Strings gibt: var nowDate = new Date() var nowArray = new Array() nowArray = nowDate.split(" ")
Wenn Sie eine Instanz des Date-Objekts als String behandeln wollen, sollten Sie diese explizit in einen String umwandeln: var newObj = new Date() var str = newObj.toString()
Die Methode tostring() konvertiert das gegebene Objekt in ein String-Objekt. Dieser Vorgang ist so ähnlich wie das „Casting“ in anderen Programmiersprachen.
5.3 Parameter der Date-Konstruktionsfunktion Bis jetzt haben wir Instanzen des Date-Objekts nur ohne jegliche Parameter erstellt; wir haben also die Instanzen entsprechend den vordefinierten Werten erstellt. Der Standardwert ist das aktuelle Datum und die aktuelle Zeit auf dem Clientrechner. In JavaScript können Sie aber ebenso Instanzen von bestimmten Daten erstellen und diese später in der Datumsrechnung benutzen. 118
Konventionen zur Repräsentation des Datums
Hier ist ein Beispiel, bei dem eine Instanz eines vergangenen Datums erstellt wird: var Xmas2000 = new Date("December 25, 2000 13:30:00")
Sie können außerdem eine Datums-Instanz erstellen, indem Sie eine Reihe von Integerwerten in dem folgenden Format angeben: Jahr, Monat, Tag, Stunde, Minute, Sekunde Hier ist ein Beispiel, bei dem dieses Format benutzt wurde: var Xmas99 = new Date(99,11,25,9,30,0)
In diesem Beispiel wurde eine zweistellige Jahreszahl gewählt, was zwar möglich, aber keinesfalls empfehlenswert ist. Die daraus resultierenden Probleme beim Jahrhundertwechsel haben wir in den letzten Jahren wohl zur Genüge kennen gelernt. Für Datumsangaben ab dem Jahr 2000 ist die vierstellige Schreibweise ohnehin notwendig, weil bei der Angabe „00“ vom Jahr 1900 ausgegangen wird. Die verbesserte Schreibweise lautet: var Xmas2000 = new Date(2000,11,25,9,30,0)
Außerdem ist zu beachten, dass die Angaben von Jahr, Monat und Tag bei beiden Formaten (String- und Integerschreibweise) unbedingt notwendig sind. Wenn Sie die Stunden, Minuten und Sekunden weglassen, werden sie einfach auf null gesetzt. Wenn Sie aber eins der ersten drei Argumente weglassen, wird dadurch ein Fehler erzeugt und unter Umständen sogar der Webbrowser zum Absturz gebracht.
5.4 Konventionen zur Repräsentation des Datums Zur Repräsentation von Daten in JavaScript werden, wie auch bei Java, Integerwerte benutzt. Zum Beispiel ist der erste Tag eines Monats 1, der zweite ist 2, der dritte ist 3 und so weiter. Die benutzten Zahlen sind aber nicht immer so ersichtlich. Die meisten Datumsattribute fangen nämlich bei 0 an. Die erste Minute einer Stunde wird wider Erwarten durch eine 0 repräsentiert, die zweite durch eine 1, die dritte durch eine 2 und so weiter. Die folgende Tabelle zeigt die numerischen Konventionen für die Attribute der Date-Instanzen: Exploring JavaScript
119
5 – Zeit und Datum in JavaScript Date-Attribut
Wertebereich
Sekunden
0 - 59
Minuten
0 - 59
Stunden
0 - 23
Tag
0-6
Datum
1-31
Monat
0 - 11
Jahr
vierstellige Jahreszahl
Tabelle 5.1: Wertebereiche der Date-Attribute
Bei clientseitigem JavaScript sollten Sie besonders beachten, dass sich Datumsund Zeitangaben immer auf den Client beziehen. Jeder Wert wird von dem Browser an das Script übergeben. Wie Sie wahrscheinlich wissen, haben alle Programme Zugriff auf die Systemzeit des Rechners. Wenn diese Uhr falsch geht, wird auch JavaScript die falschen Werte benutzen und eventuell überraschende Ergebnisse hervorrufen.
5.5 Kategorien der Date-Methoden JavaScript beinhaltet bereits eine Vielzahl an Methoden zur Manipulation von Instanzen des Date-Objekts. Durch die Möglichkeit, Daten auszugeben und sie zu manipulieren, werden sie zu den wichtigsten Elementen dieser Programmiersprache. Die Vielzahl an Methoden wird Sie auf den ersten Blick wahrscheinlich überfordern. Daher haben wir uns entschlossen, sie in vier Teile aufzuteilen, bezogen auf ihre Funktion: 쐌 Get-Methoden 쐌 Set-Methoden 쐌 To-Methoden 쐌 Parse-Methoden get-Methoden geben einen Integerwert zurück, entsprechend dem Attribut der Instanz, das Sie wünschen. Mit get-Methoden können Sie zum Beispiel das Jahr abfragen („get“), oder die Zahl des Monats. set-Methoden ermöglichen es, den Wert eines bestimmten Attributs einer bestehenden Instanz zu verändern. Diese 120
get-Methoden
Methoden erwarten Integerargumente anstatt welche zurückzuliefern. Mit diesen Anweisungen setzen („set“) Sie die Attribute einer Instanz auf einen bestimmten Wert. to-Methoden konvertieren das Datum in einen String entsprechend den Argumenten, die Sie den Methoden übergeben. Danach können Sie die Vorteile des Stringformats mit Stringmethoden und Stringeigenschaften nutzen, wie zum Beispiel die Methode split(). parse-Methoden interpretieren die Daten, die ihnen übergeben werden.
5.6 get-Methoden 5.6.1
getYear()
Die getYear()-Methode gibt das momentane Jahr aus, das in einer Instanz des Date-Objekts gespeichert wurde. Früher war dies eine zweistellige Zahl, wobei angenommen wurde, dass es sich um das 20. Jahrhundert handelte. In den neueren Versionen werden aber aufgrund der Jahr-2000-Kompatibilität vierstellige Werte zurückgegeben. var now = new Date() var year = now.getYear() while (1) { guessYear = prompt("Welches Jahr haben wir? ", "") var guessYear = parseInt(guessYear) if (guessYear == year) { alert("Richtig!") break } else alert("Falsch! Versuchen Sie es noch einmal.") }
Die Ausgabe dieses Skripts, angenommen Sie geben die richtige Antwort, sieht so aus, wie in den Abbildungen 5.2 und 5.3 gezeigt wird:
Exploring JavaScript
121
5 – Zeit und Datum in JavaScript
Abbildung 5.2: Eine Dialogbox, die nach dem aktuellen Jahr fragt
Abbildung 5.3: Eine Alertbox, die eine Antwort gibt
Das momentane Jahr (basierend auf der Systemzeit) wird von der Instanz now bezogen, die entsprechend den vordefinierten Argumenten (aktuelle Zeit und Datum) erstellt wurde. Dann wird eine Schleife ohne eine Endbedingung gestartet. Der Benutzer wird nach der aktuellen Jahreszahl gefragt. Eine if-Anweisung überprüft, ob der Benutzer das richtige Jahr angegeben hat. Wenn das der Fall ist, wird die richtige Nachricht ausgegeben, und die Schleife wird mit einer Break-Anweisung unterbrochen. Anderenfalls wird eine andere Nachricht ausgegeben, die den Benutzer über seine falsche Antwort informiert, und die Schleife wird wiederholt. Unter Umständen kann es bei alten Versionen des Netscape Navigators dazu kommen, dass die Jahreszahlen im Verhältnis zu 1900 ausgegeben werden, das Jahr 2001 wäre demnach 101. Mit dem neueren Befehl getFullYear erhalten Sie auch mit diesen Browsern eine vierstellige Jahreszahl.
5.6.2
getMonth()
Die Methode getMonth() extrahiert den Monat aus dem aufrufenden Objekt und gibt ihn zurück. Die Monate gehen von Januar bis Dezember, genauer gesagt von 0 bis 11. Das folgende Beispiel zeigt, wie die Methode funktioniert, und enthält außerdem eine Array-Instanz:
122
get-Methoden var now = new Date() var month = now.getMonth() var ar = new Array(12) ar[0] = "Januar" ar[1] = "Februar" ar[2] = "März" ar[3] = "April" ar[4] = "Mai" ar[5] = "Juni" ar[6] = "Juli" ar[7] = "August" ar[8] = "September" ar[9] = "Oktober" ar[10] = "November" ar[11] = "Dezember" var message = "Es ist jetzt " + ar[month] message += ", mein Lieblingsmonat! " document.write(message)
Die Ausgabe dieses Skripts wird in Abbildung 5.4 gezeigt:
Abbildung 5.4: Beispielausgabe mit Benutzung der getMonth()-Methode
Exploring JavaScript
123
5 – Zeit und Datum in JavaScript
Der momentane Monat wird von der Instanz now bezogen, in der die Attribute der aktuellen Zeit gespeichert sind (sobald die Instanz erstellt wurde). Danach wird ein statisches Array erstellt, in dem alle Namen der Monate als String gespeichert werden. Die Reihenfolge fängt bei 0 an und es wird dabei immer der zur Nummer entsprechende Monat gespeichert. Bei 0 anzufangen ist bei der Arraystruktur ohnehin angenehm. Daher wird keine Mathematik benötigt, und mit dem momentanen Monat bzw. dessen Namen wird eine Nachricht erstellt. Diese Nachricht wird dann als HTML ausgegeben.
5.6.3
getDate()
Wenn die getDate()-Methode von einem Objekt aufgerufen wurde, gibt sie das Datum als Integerwert zwischen 1 und 31 zurück. Hier ist ein Beispiel: var now = new Date() var year = now.getYear() var month = now.getMonth() var date = now.getDate() // Array für die Namen der Monate var ar = new Array(12) ar[0] = "Januar" ar[1] = "Februar" ar[2] = "März" ar[3] = "April" ar[4] = "Mai" ar[5] = "Juni" ar[6] = "Juli" ar[7] = "August" ar[8] = "September" ar[9] = "Oktober" ar[10] = "November" ar[11] = "Dezember" // Ein vollständiges Datum wird erstellt, wie z.B. // "10. September 2001" var totalDate = date + ". " + ar[month] + " " + year document.write(totalDate)
124
get-Methoden
Die Ausgabe dieses Skripts wird in Abbildung 5.5 gezeigt:
Abbildung 5.5: Beispielausgabe mit Benutzung der getDate()-Methode
In diesem Skriptsegment werden alle drei Methoden, die wir bisher kennen gelernt haben, kombiniert, um ein vollständiges und ordentlich formatiertes Datum auszugeben. Zuerst werden die drei Attribute von der zuvor erstellten Date-Instanz now bezogen und den entsprechenden Variablen zugeordnet. Dann wird ein Array mit den Namen der Monate, wie schon im vorherigen Beispiel, erstellt. Dann werden die drei Teile zusammengesetzt, zur Trennung werden Punkt und Leerzeichen eingefügt. Das vollständige Datum wird danach ausgegeben.
5.6.4
getDay()
Diese Methode gibt den Wochentag als eine Zahl zwischen 0 und 6 zurück. Der Tag wird entsprechend den übrigen Attributen berechnet, daher gibt es keine setDay()-Methode. Hier ist ein Beispiel: ar = new Array(7) ar[0] = "Sonntag" ar[1] = "Montag"
Exploring JavaScript
125
5 – Zeit und Datum in JavaScript ar[2] = "Dienstag" ar[3] = "Mittwoch" ar[4] = "Donnerstag" ar[5] = "Freitag" ar[6] = "Samstag" var birthday = new Date("April 4, 1999") var day = birthday.getDay() alert("Lukas ist an einem " + ar[day] + " geboren worden!")
5.6.5
getHours()
Die Funktion getHours() gibt die Zahl der Stunden seit Mitternacht zurück. Sie gibt also die momentane Stunde gemäß einer 24-Stunden-Uhr aus. Der gültige Bereich ist 0-23. Hier ist ein Beispiel: var now = new Date() var hour = now.getHours() var text = "" if (hour < 12) text = "Morgen" else if (hour < 17) text = "Tag" else if (hour < 22) text = "Abend" else text = "Nacht" document.write("Gute(n) " + text + "!")
Dieses Skript erstellt eine kurze Begrüßung entsprechend der momentanen Tageszeit. Zwischen 12-17 Uhr gibt es "Gute(n) Tag!" aus. Es basiert auf verschachtelten if-else-Anweisungen.
5.6.6
getMinutes()
Die Methode getMinutes() gibt das Minuten-Attribut einer Date-Instanz zurück. Die zurückgegebene Zahl liegt immer zwischen 0 und 59. Hier ist ein kurzes Beispiel zur Demonstration dieser Methode:
126
get-Methoden var now = new Date() var minute = now.getMinutes() var hour = now.getHours() var text = "Haben Sie nicht einen Termin um " + (hour + 1) text += ":00 ? " var timeleft = 60 – minute text += "Sie haben noch " + timeleft + " Minuten Zeit... " document.write(text)
Zuerst wird die Frage erstellt, wobei die Stunde einfach um eine erhöht wird und die Minutenzahl auf 00 gesetzt wird. Wenn es zum Beispiel 15:55 ist, wird die Uhrzeit 16:00 in die Frage eingesetzt. Danach erhält die Variable timeleft die Zahl der verbleibenden Minuten bis zur nächsten vollen Stunde. Der Wert der Variable minute wurde mit der Methode getMinutes() erfasst. Zu der Variablen text wird dann noch die Information über die verbleibende Zeit hinzugefügt und mit der folgenden Anweisung der gesamte String ausgegeben. Als Randbemerkung sei gesagt, dass dieses Skript fälschlicherweise den nächsten Termin für 24:00 ansetzt, wenn es nach 23 Uhr ist.
5.6.7
getSeconds()
Diese Methode gibt die Sekunden einer gegebenen Date-Instanz zurück. Die Werte liegen zwischen 0 und 59. Die Funktionsweise ist wie bei der getMinutes()Methode.
5.6.8
getTimezoneOffset()
Die Methode getTimezoneOffset() gibt den Zeitzonenunterschied zur Greenwich Mean Time (GMT) des momentanen Standorts aus. Aufgrund der Sommer- und Winterzeitumstellung sind diese Werte nicht konstant. Der zurückgegebene Wert ist ein Integerwert, der den Unterschied in Minuten repräsentiert. Das folgende Skript zeigt, wie man mit dem Zeitzonenunterschied des Benutzers herausfinden kann, wo er sich befindet: var now = new Date() var curOffset = now.getTimezoneOffset() curOffset /= 60 // Konvertierung von Minuten in Stunden var zone = "" var summertime = confirm("Ist momentan Sommerzeit?") if (summertime == true) { summertime = 1 }
Exploring JavaScript
127
5 – Zeit und Datum in JavaScript else { summertime = 0 } curOffset = curOffset + summertime if (curOffset == -1) { zone = "(GMT +01:00) Amsterdam, Berlin, Bern, Rom, " zone += "Stockholm, Wien" } else { zone = "(GMT " + curOffset + ")" } alert("Sie befinden sich in folgender Zeitzone:\n" + zone )
Zuerst wird dem Benutzer die Frage gestellt, ob momentan Sommerzeit ist. Ist dies der Fall, so wird der Zeitzonenunterschied curOffset mit der Variablen summertime relativiert. Beachten Sie, dass hier eine Boolesche Variable automatisch in eine Integervariable umgewandelt wurde. In der folgenden if-Bedingung wird überprüft, ob sich der Benutzer in der Zeitzone -1 befindet, was zum Beispiel Deutschland entsprechen würde. Falls nicht, wird lediglich die Differenz zur GMT ausgegeben.
5.6.9
getTime()
Die Methode getTime() gibt die Zahl der Millisekunden seit dem 1. Januar 1970 00:00:00 aus.
5.7 set-Methoden 5.7.1
setYear()
Diese Methode setzt das Jahr-Attribut einer gegebenen Date-Instanz. Das folgende Beispiel berechnet den Tag des momentanen Datums vor einem Jahr: var now = new Date() var year = now.getYear() now.setYear(year - 1) ar = new Array(7) ar[0] = "Sonntag" ar[1] = "Montag" ar[2] = "Dienstag"
128
set-Methoden ar[3] = "Mittwoch" ar[4] = "Donnerstag" ar[5] = "Freitag" ar[6] = "Samstag" document.write("Letztes Jahr war das heutige Datum ein ") document.write(ar[now.getDay()])
Zuerst wird eine Instanz des momentanen Datums erstellt und das momentane Jahr der Variablen year zugewiesen. Dann wird das Jahrattribut von der Instanz now ein Jahr zurückgesetzt. Das Tagattribut wird dann von der veränderten Instanz bezogen und eine Nachricht basierend auf diesem Tag erstellt. Dabei wird das gleiche Array wie bei dem Beispiel zu getDay() benutzt.
5.7.2
setMonth()
Die Methode setMonth() setzt das Monatsattribut einer gegebenen Instanz des Date-Objekts auf einen bestimmten Wert. Das folgende Skript setzt das Monatsattribut des momentanen Datums auf den Monat Mai: var now = new Date() now.setMonth(4)
5.7.3
setDate()
Diese Methode setzt das Datumsattribut einer gegebenen Date-Instanz auf einen bestimmten Wert. Das folgende Skript gibt den Wochentag des ersten Tages dieses Monats aus: var now = new Date() now.setDate(1) ar = new Array(7) ar[0] = "Sonntag" ar[1] = "Montag" ar[2] = "Dienstag" ar[3] = "Mittwoch" ar[4] = "Donnerstag" ar[5] = "Freitag" ar[6] = "Samstag" document.write("Der erste Tag dieses Monats war ein ") document.write(ar[now.getDay()])
Exploring JavaScript
129
5 – Zeit und Datum in JavaScript
5.7.4
setHours()
Diese Methode setzt das Stundenattribut einer gegebenen Instanz des DateObjekts auf einen bestimmten Wert. Hier ist ein Beispiel: var obj = new Date("October 10, 2001 19:50:59") // Stunden = 19 obj.setHours(obj.getHours() - 2) alert(obj.getHours()) // Stunden = 17
5.7.5
setMinutes()
Diese Methode setzt die Minuten einer gegebenen Date-Instanz auf einen neuen Wert. Hier ist ein einfaches Beispiel: var obj = new Date("October 10, 2001 19:50:59") // Minuten = 50 obj.setMinutes(obj.getMinutes() – 1) alert(obj.getMinutes()) // Minuten = 49
5.7.6
setSeconds()
Die Methode setSeconds() setzt die Sekunden einer gegebenen Instanz des DateObjekts auf einen bestimmten Wert. Das folgende Beispiel demonstriert die Benutzung: var obj = new Date("October 10, 2001 19:50:59") // Sekunden = 59 obj.setSeconds(obj.getSeconds() – 9) alert(obj.getSeconds()) // Sekunden = 50
5.7.7
setTime()
Diese Methode setzt die Zahl der Millisekunden seit dem 1. Januar 1970, 00:00:00, auf den gegebenen Wert. Sie modifiziert daher alle Attribute des aufrufenden Objekts. Hier ist ein Beispiel: var obj = new Date() obj.setTime(867999600000) var date = obj.getDate() var month = obj.getMonth() var year = obj.getFullYear() // Array für die Namen der Monate
130
to-Methoden var ar = new Array(12) ar[0] = "Januar" ar[1] = "Februar" ar[2] = "März" ar[3] = "April" ar[4] = "Mai" ar[5] = "Juni" ar[6] = "Juli" ar[7] = "August" ar[8] = "September" ar[9] = "Oktober" ar[10] = "November" ar[11] = "Dezember" var text = date + ". " + ar[month] + " " + year document.write(text)
5.8 to-Methoden 5.8.1
toGMTString()
Diese Methode konvertiert ein Datum in einen String, entsprechend den Internet GMT- Konventionen. Die Konvertierung erfolgt entsprechend dem im Betriebsystem eingestellten Zeitzonenunterschied. Es wird ein String erstellt, der ähnlich ist wie folgender: Tue, 30 Jul 2001 01:03:46 GMT
Die genaue Schreibweise hängt von Betriebssystem und Rechner ab. Hier ist ein Beispiel: var now = new Date() var ar1 = now.toGMTString().split(" ") document.write("The current time in Greenwich is " + ar1[4])
Eine Ausgabe könnte beispielsweise so aussehen: The current time in Greenwich is 21:03:51
Exploring JavaScript
131
5 – Zeit und Datum in JavaScript
5.8.2
toLocaleString()
Diese Methode gibt das Datum in der Form eines Strings zurück, entsprechend den lokalen Konventionen. Wenn Sie versuchen, mit dieser Funktion ein Datum zu übergeben, sollten Sie beachten, dass hier sehr unterschiedliche Ergebnisse entstehen können, abhängig vom momentanen Standort. Die Methoden getHours, getMinutes und getSeconds liefern in der Regel leichter zu portierende Ergebnisse. Das folgende Beispiel zeigt die Funktionsweise dieser Methode: var now = new Date() var ar1 = now.toLocaleString().split(" ") document.write("Die momentane Uhrzeit ist " + ar1[1])
Die Ausgabe dieses Skripts ist: Die momentane Uhrzeit ist 16:09:53
Das allgemeine Format des konvertierten Strings ist: MM/TT/JJ SS:MM:SS
5.9 parse-Methoden 5.9.1
parse()
Die Methode parse() nimmt einen Datumsstring nach dem IETF-Standard an und konvertiert ihn in die Zahl der Millisekunden seit dem 1. Januar 1970, 00:00:00. Der IETF-Standard für die Datumsschreibweise ist: Tagabkürzung, Datum Monatsabkürzung Jahr HH:MM:SS Zeitzonenabkürzung Ein Beispiel für diesen Standard ist „Mon, 24 Dec 2001 18:30:00 GMT“. Diese Methode versteht außerdem die Zeitzonen der USA wie PST (Pacific Standard Time) und EST (Eastern Standard Time). Für die Zeitzonen außerhalb der USA gibt es jedoch keine Zeitzonenabkürzungen, die durch JavaScript verarbeitet werden. Für solche Zeitzonen muss der Zeitzonenunterschied angegeben werden. Mit Zeitzonenunterschied ist dabei die Differenz zwischen der Greenwich Mean Time und der lokalen Zeit gemeint. Zum Beispiel würde bei der Angabe “Mon, 25 Dec 2001 13:30:00 GMT+0430” das GMT+0400 für 4 Stunden westlich der Greenwich Mean Time stehen. Wenn man keine Zeitzone angibt, wird 132
Zeitbezogene Methoden anderer Objekte
die lokale Zeit entsprechend der Systemeinstellung des Computers benutzt. Falls Ihre Zeitzone nicht korrekt eingestellt ist, sollten Sie diese in der Systemsteuerung beim Mac oder unter Windows anpassen. GMT ist auch unter dem Namen Universal Coordinate Time, abgekürzt UTC, bekannt. Die parse-Methode ist statisch. Sie gehört nicht zu einer bestimmten Instanz, sondern zum Date-Objekt selbst. Daher wird sie immer mit Date.parse() aufgerufen. Hier ist ein Beispiel zu dieser Methode: var aDate = "April 4 1999" var birthday = new Date() birthday.setTime(Date.parse(aDate))
5.9.2
UTC()
Die Methode UTC() erwartet eine durch Kommata getrennte Liste der Datumsangaben und gibt die Zahl der verstrichenen Millisekunden seit dem 1. Januar 1970, 00:00:00 nach Greenwich Mean Time (GMT, UTC) aus. Sie ist ebenfalls statisch und wird nur in Verbindung mit dem Date-Objekt aufgerufen. Sie können sich mit dieser Methode nicht auf ein Datum in der lokalen Zeitzone beziehen, weil immer von der Universal Time Coordinate (GMT, UTC) ausgegangen wird. Die folgende Anweisung erstellt ein Date-Objekt, das die GMT anstelle der lokalen Zeit benutzt: gmtDate = new Date(Date.UTC(99, 11, 1, 0, 0, 0))
Die allgemeine Syntax dieser Methode lautet: Date.UTC(year, month, day [, hrs] [, min][,sec])
Die Attribute sollten als Integerwerte angegeben werden.
5.10 Zeitbezogene Methoden anderer Objekte 5.10.1 setTimeout() Die Methode setTimeout() wertet einen Ausdruck aus, nachdem eine bestimmte Zahl an Millisekunden vergangen ist. Die allgemeine Syntax lautet: timeoutID = setTimeout(expression, msec) timeoutID ist ein Bezeichner, mit dem der Timeout identifiziert wird.
Exploring JavaScript
133
5 – Zeit und Datum in JavaScript expression ist ein String-Ausdruck oder eine Eigenschaft eines bestehenden Objekts. Es ist normalerweise eine simple Anweisung, die ausgeführt werden soll, wenn die angegebene Zeit verstrichen ist. msec ist ein numerischer Wert, ein numerischer String oder eine Eigenschaft eines bestehenden Objekts in Millisekunden.
Hier ist ein Beispiel, um die Funktion zu verdeutlichen: <TITLE>setTimeout() <SCRIPT LANGUAGE="JavaScript"> function displayAlert() { alert("Die 5 Sekunden sind vorbei!") }
Wenn Sie auf den Button klicken, setzt das Skript des Event-Handlers einen Timeout. In der Timeout-Anweisung steht, dass nach 5000 Millisekunden, oder fünf Sekunden, die Funktion displayAlert() aufgerufen wird. Daher wird fünf Sekunden nachdem Sie auf den Button geklickt haben eine Alertbox angezeigt. Diese Methode führt die Funktion nicht mehrmals hintereinander aus. Sie ruft die Anweisung also nicht alle fünf Sekunden auf. Wenn die angegebene Zeit verstrichen ist, wird die Anweisung ausgeführt und der Timeout existiert nicht mehr. SetTimeout() ist eine Methode des window- oder frame-Objekts und damit abhängig von der Struktur des HTML-Dokuments, in dem sie benutzt wird.
134
Zeitbezogene Methoden anderer Objekte
Die setTimeout-Methode wird häufig dazu genutzt, eine Pause zwischen zwei Aufrufen einer rekursiven, benutzerdefinierten Funktion zu machen. Schauen Sie sich folgendes Skript an: function alertNumbers(num) { if (num > 10) return alert(num) val = ++num timerID = setTimeout("alertNumbers(val)", 3000) } alertNumbers(0)
Durch dieses Skript öffnet sich alle drei Sekunden eine Alertbox mit der aktuellen Nummer. In der ersten Alertbox steht eine 0, die zweite öffnet sich drei Sekunden später mit einer 1. Dieser Vorgang wiederholt sich bis zur Nummer 10. Falls Sie versuchen, die Ausgabe in die HTML-Datei zu leiten, wird ein Fehler entstehen. Der Grund dafür ist, dass Sie durch diese Aktion versuchen das HTML-Layout zu verändern, welches zu diesem Zeitpunkt aber bereits festgelegt ist. Ein weiterer zu beachtender Punkt ist, dass, falls die Anweisung, die in der setTimeout()-Methode ausgeführt wird, wie in diesem Beispiel eine Funktion ist, die ein Argument benötigt, dieses Argument eine globale Variable sein muss. Lokale Variablen funktionieren nicht, da setTimeout() eine Methode des windowbzw. frame-Objekts ist (window ist das Standardobjekt, wenn kein anderes angegeben wurde). Sie können allerdings auch ein Literal als Argument benutzen. Merken Sie sich, dass setTimeout() einen Ausdruck benötigt, der von Anführungszeichen eingeschlossen wird. Wie in den folgenden Zeilen gezeigt, können Sie stattdessen auch eine lokale Variable in diesem Ausdruck benutzen: var cmd = "foo(" + num + ")" timerID = setTimeout(cmd, 2000) // oder eine beliebige andere Zeit
5.10.2 clearTimeout() Diese Methode bricht einen Timeout ab, der zuvor mit der setTimeout()Methode gesetzt wurde. Sie ist ebenfalls Teil des window- bzw. frame-Objekts und wird daher später noch einmal genauer besprochen. Im Moment ist es wichtig,
Exploring JavaScript
135
5 – Zeit und Datum in JavaScript
dass Sie wissen, wie man mit diesem Befehl einen Timeout abbrechen kann. Die allgemeine Schreibweise lautet: clearTimeout(timeoutID)
timeoutID ist eine Timeout-Einstellung, die bei einem zuvor ausgeführten Aufruf der setTimeout()-Methode zurückgeliefert wurde. Sie muss genau dieselbe sein, die mit der setTimeout()-Methode benutzt wurde, da die Timeout-Einstellung durch sie identifiziert wird.
Die setTimeout()-Methode setzt einen Timeout fest; wenn Sie diese tickende Bombe entschärfen wollen, können Sie die Methode clearTimeout() verwenden. Wenn Sie die ablaufende Zeit verändern wollen, müssen Sie den Timeout zuerst löschen und dann einen neuen festlegen. Hier ist noch einmal das obige Beispiel, diesmal mit der clearTimeout()-Methode erweitert: <TITLE>setTimeout()+clearTimeout() <SCRIPT LANGUAGE="JavaScript"> function displayAlert() { alert("Die 5 Sekunden sind vorbei!") }
136
Beispiele zu Zeit und Datum
5.11 Beispiele zu Zeit und Datum Zeiten und Daten werden häufig in Skripten verwendet, um vielerlei Ziele zu erreichen und unterschiedliche Effekte zu erschaffen. In diesem Abschnitt werden wir einige nützliche Beispiele und mögliche Anwendungen zeigen, in denen die in diesem Kapitel vorgestellten Methoden eingesetzt werden.
5.11.1 Eine Digitaluhr mit AM/PM Die Zeit- und Datumsmethoden sind nützlich, wenn man Zeitwerte in JavaScript-Skripten berechnen will. Das folgende Beispiel demonstriert, wie man diese Methoden benutzen kann, um eine hübsche Digitaluhr in eine HTMLSeite einzufügen: <TITLE> JavaScript am/pm Digitaluhr <SCRIPT LANGUAGE="JavaScript">
Listing 5.1: (bsp12.01.htm) Eine einfache graphische Uhr, basierend auf den Zeit- und Datumsmethoden
Exploring JavaScript
137
5 – Zeit und Datum in JavaScript // Anleitung: // ************* // Kopieren Sie alle Bilder in ein Verzeichnis. // Fügen Sie dieses Skript in Ihre HTML-Datei ein. // Die HTML-Datei muss sich im selben Verzeichnis // befinden wie die Bilder. document.write(setClock()) function setClock() { // die zu erweiternde HTML-Variable wird mit // einem leeren String initialisiert. var text = "" // Vorgaben für die Bild-Tags werden festgelegt var openImage = "" // Initialisierung der zeitbezogenen Variablen mit // aktuellen Werten var now = new Date() var hour = now.getHours() var minute = now.getMinutes() now = null var ampm = "" // der Stundenwert wird ausgewertet und die Variable // ampm entsprechend gesetzt. if (hour >= 12) { hour = hour - 12 ampm = "pm" } else { ampm = "am" } hour = (hour == 0) ? 12 : hour // Bei einzahligen Minuten wird eine 0 hinzugefügt. if (minute < 10) { minute = "0" + minute // do not parse this number! } // minute und hour werden in Strings umgewandelt minute += "" hour += ""
Listing 5.1: (bsp12.01.htm) Eine einfache graphische Uhr, basierend auf den Zeit- und Datumsmethoden (Forts.)
138
Beispiele zu Zeit und Datum // image-Tags werden entsprechend der Stunden generiert for (var i = 0; i < hour.length; ++i) { text += openImage + hour.charAt(i) + closeImage } // Doppelpunkt zur Trennung wird eingefügt text += openImage + "c.gif\" HEIGHT=21 WIDTH=9>" // image-Tags werden entsprechend der Minuten generiert for (var i = 0; i < minute.length; ++i) { text += openImage + minute.charAt(i) + closeImage } // image-Tag für am/pm wird eingefügt text += openImage + ampm + closeImage // Die Ausgabevariable text, in der die HTML// Befehle stehen, wird zurückgegeben. return text } function getPath(url) { lastSlash = url.lastIndexOf("/") return url.substring(0, lastSlash + 1) } // -->
Listing 5.1: (bsp12.01.htm) Eine einfache graphische Uhr, basierend auf den Zeit- und Datumsmethoden (Forts.)
Exploring JavaScript
139
5 – Zeit und Datum in JavaScript
Die Ausgabe dieses Skripts wird in Abbildung 5.6 gezeigt:
Abbildung 5.6: Eine graphische Uhr, die mit den Zeit- und Datumsmethoden realisiert wurde
Im ersten Teil des Skripts stehen eine Erklärung zur Benutzung des Skripts sowie die benötigten Dateien. Es ist wichtig, einen solchen Kommentar am Anfang eines Skripts einzufügen, da es ansonsten schwierig ist, herauszufinden, welche Dateien benötigt werden und wofür das Skript überhaupt gut ist. Die Funktion getPath erwartet die Adresse des aktuellen Dokuments. Dies beinhaltet alle Teile des URL, inklusive „http://“ und den Namen der Datei. Die Funktion gibt dann den URL ohne den Dateinamen aber inklusive dem letzten Slash (/) zurück. Nehmen wir zur Erläuterung das Beispiel: http://www.entwickler.com/index.html
Nach der Übergabe dieses Strings an die Funktion würde sie http://www.entwickler.com/
zurückgeben. 140
Beispiele zu Zeit und Datum
Die erste Funktion in dem Skript, setClock(), erstellt die gesamte Uhr. Die jeweiligen Abschnitte dieser Funktion werden durch Kommentare erklärt. Schauen Sie sich folgende Anweisungen an, die direkt aus dem Skript kopiert wurden: var openImage = ""
Hier werden zwei Konstanten-ähnliche Variablen deklariert und mit einem sinnvollen Wert initialisiert. Der ersten wird der Anfang eines -Tags aus HTML zugewiesen. Beachten Sie, dass location.href der aktuelle URL zu dem HTML-Dokument ist. Beachten Sie außerdem die Benutzung von Escape-Zeichen (\"). Der Wert der Variablen closeImage ist unabhängig von jeglichen Einflüssen wie zum Beispiel dem Pfad zu der Datei. Ihm wird daher lediglich das Ende des Tags zugewiesen. Die Attribute HEIGHT und WIDTH entsprechen der Größe der Zahlen-Bilder, mit denen die Zeit dargestellt wird. Hier ist der nächste Teil des Skripts: var var var now var
now = new Date() hour = now.getHours() minute = now.getMinutes() = null ampm = ""
Dieser Abschnitt erledigt zwei wichtige Aufgaben: 쐌 Die aktuelle Stunde wird hour zugewiesen. 쐌 Das minute-Attribut der aktuellen Zeit wird der Variablen minute zugewiesen. Wenn Ihnen diese Zeilen nicht klar sind, schauen Sie noch einmal am Anfang dieses Kapitels nach. Der nächste Abschnitt modifiziert den Wert von hour so, dass er dem in den USA benutzen 12-Stunden System entspricht. Zeiten nach 12:00 mittags werden mit P.M. geschrieben und Mitternacht wird mit 12:00 beschrieben, nicht mit 0:00. Schauen Sie sich folgende Anweisung an: if (minute < 10) minute = "0" + minute // Achtung, dies ist ein String!
Exploring JavaScript
141
5 – Zeit und Datum in JavaScript
Diese Anweisung stellt sicher, dass in der Variablen minute eine zweistellige Minutenzahl gespeichert ist. Falls es ursprünglich eine einstellige Minutenzahl war, wird eine führende „0“ hinzugefügt. Beachten Sie, dass diese Zahl eigentlich ein String ist. Wenn man versuchen würde, sie mit einer Funktion wie parseInt() zu verarbeiten, würde sie in einen numerischen Wert umgewandelt und damit ihr Wert verändert, da sie mit einer führenden 0 als Oktalzahl gilt. Sie muss also im gesamten Skript ihren Stringwert behalten. Die folgenden beiden Anweisungen wandeln die Werte von hour und minute in Strings um, indem ein leerer String mit dem Verknüpfungsoperator an sie gefügt wird. Das ist wichtig, da String-Eigenschaften, die nur mit Strings verwendet werden können, später im Skript mit ihnen benutzt werden. Die nächste Anweisung ist eine Schleife: for(var i =0;i
Die Schleife wird so oft aufgerufen, wie der Wert von hour.length ist. Das heißt, wenn die Stunde zweistellig ist, wird die Schleife zweimal aufgerufen, für jede Stelle einmal. Bei jedem Ablauf wird ein Image-Tag entsprechend der momentanen Zahl, die in hour.charAt(i) gespeichert ist, zu der Ausgabevariablen text hinzugefügt. Wenn die Stundenzahl zum Beispiel 12 ist, wird der Anweisungsblock zweimal aufgerufen. Während des ersten Durchlaufs wird der folgende String der Variablen text zugewiesen: text += ''
Beim zweiten Durchlauf wird diese Anweisung entsprechend aufgerufen: text += ''
Wenn der Wert von hour ein Ein-Zeichen-String ist, wird die Schleife nur einmal ausgeführt. Die nächste Anweisung im Skript ist: text += openImage + "c.gif\" HEIGHT=21 WIDTH=9>"
142
Beispiele zu Zeit und Datum
Diese Anweisung setzt den Doppelpunkt, der zwischen den Stunden und Minuten stehen soll, ein. Beachten Sie, dass hierbei nicht die schließende Variable closeImage benutzt wurde, weil das WIDTH-Attribut des Doppelpunkt-Bildes anders als bei den Zahlen-Bildern ist. Die folgende Schleife ist genauso aufgebaut wie die Schleife für die Stunden, die wir eben besprochen haben. Der Unterschied ist lediglich, dass sie sich auf die Variable minute bezieht. Das AM- oder PM-Bild wird entsprechend dem Wert der Variablen ampm zur Ausgabevariablen text hinzugefügt. Die letzte Anweisung dieser Funktion befiehlt JavaScript, den Wert der Variablen text zurückzugeben, in der die gesamten HTML-Anweisungen zur Darstellung der Uhr gesammelt wurden. Der zurückgegebene Wert wird dann von einer globalen Anweisung ausgegeben - document.write(text).
5.11.2 Digitales Datum Dieses Beispiel ist so ähnlich wie das vorherige, mit dem Unterschied, dass diesmal eine Bilderfolge bezogen auf das Datum anstelle der Zeit ausgegeben wird. Es basiert auf denselben Funktionen, daher können wir uns wohl eine detaillierte Erklärung sparen. Hier ist das Skript: <TITLE> Datumsausgabe mit JavaScript <SCRIPT LANGUAGE="JavaScript">
Listing 5.2: (bsp12.02.htm) Eine einfache, graphische Ausgabe des aktuellen Datums
Exploring JavaScript
143
5 – Zeit und Datum in JavaScript // dg3.gif // dg4.gif // dg5.gif // dg6.gif // dg7.gif // dg8.gif // dg9.gif // dgp.gif // Jeder beliebige Satz von Zahlenbildern (0-9) und ein Bild // mit einem Punkt (.) funktionieren mit diesem Skript. // Anleitung: // ************* // Kopieren Sie alle Bilder in ein Verzeichnis. // Fügen Sie dieses Skript in Ihre HTML-Datei ein. // Die HTML-Datei muss sich im selben Verzeichnis // befinden wie die Bilder. document.write(setDate()) function setDate() { // die zu erweiternde HTML-Variable wird mit // einem leeren String initialisiert. var text = "" // Vorgaben für die Bild-Tags werden festgelegt var openImage = "" // Initialisierung der datumsbezogenen Variablen // mit aktuellen Werten var now = new Date() var month = now.getMonth() var date = now.getDate() var year = now.getYear() now = null // Der Integerwert von month wird auf den // Standardbereich konvertiert month++ //0 –11 =>1 –12 // Die Werte von minute und hour werden in // Strings konvertiert. month += "" date += ""
Listing 5.2: (bsp12.02.htm) Eine einfache, graphische Ausgabe des aktuellen Datums (Forts.)
144
Beispiele zu Zeit und Datum year += "" // Die Image-Tags für das Datum werden zur // Ausgabevariablen text hinzugefügt. for(var i =0;i " // Die Image-Tags für den Monat werden zur // Ausgabevariablen text hinzugefügt. for(var i =0;i <month.length; ++i) { text += openImage + month.charAt(i) + closeImage } // Das Image-Tag für den Punkt zur Trennung wird // zur Ausgabevariablen text hinzugefügt. text += openImage + "p.gif\" HEIGHT=21 WIDTH=9>" // Die Image-Tags für das Jahr werden zur // Ausgabevariablen text hinzugefügt. for(var i =0;i
Listing 5.2: (bsp12.02.htm) Eine einfache, graphische Ausgabe des aktuellen Datums (Forts.)
Exploring JavaScript
145
5 – Zeit und Datum in JavaScript
Die Ausgabe des Listings 5.2 wird in Abbildung 5.7 gezeigt:
Abbildung 5.7: Graphische Ausgabe des aktuellen Datums
5.11.3 Ein Kalender Bei dem nächsten Beispiel wird der aktuelle Monatskalender ausgegeben. Bevor wir den Quellcode besprechen, schauen Sie sich zuerst die Beispielsausgabe der Funktion an:
Abbildung 5.8: Ein Monats-Kalender
146
Beispiele zu Zeit und Datum
Sehen Sie sich nun das Skript selbst an: <TITLE> Kalender mit JavaScript <SCRIPT LANGUAGE="JavaScript">
Listing 5.3: (bsp12.03.htm) Ein Kalender, der auf einer HTML-Tabelle basiert und von JavaScript generiert wird
Exploring JavaScript
147
5 – Zeit und Datum in JavaScript } function getDays(month, year) { // Array anlegen mit Länge der einzelnen Monate var ar = new Array(12) ar[0] = 31 // Januar ar[1] = (leapYear(year)) ? 29 : 28 // Februar ar[2] = 31 // März ar[3] = 30 // April ar[4] = 31 // Mai ar[5] = 30 // Juni ar[6] = 31 // Juli ar[7] = 31 // August ar[8] = 30 // September ar[9] = 31 // Oktober ar[10] = 30 // November ar[11] = 31 // Dezember // gibt die Anzahl der Tage im angegebenen // Monat (Parameter) zurück return ar[month] } function getMonthName(month) { // Ein Array wird erstellt, in dem die Namen // der Monate als konstante Werte gespeichert // werden. var ar = new Array(12) ar[0] = "Januar" ar[1] = "Februar" ar[2] = "März" ar[3] = "April" ar[4] = "Mai" ar[5] = "Juni" ar[6] = "Juli" ar[7] = "August" ar[8] = "September" ar[9] = "Oktober"
Listing 5.3: (bsp12.03.htm) Ein Kalender, der auf einer HTML-Tabelle basiert und von JavaScript generiert wird (Forts.)
148
Beispiele zu Zeit und Datum ar[10] = "November" ar[11] = "Dezember" // gibt den Namen des angegebenen Monats // (Parameter) zurück return ar[month] } function setCal() { // Standard Zeit-Attribute var now = new Date() var year = now.getYear() var month = now.getMonth() var monthName = getMonthName(month) var date = now.getDate() now = null // erstellt eine Instanz mit dem ersten Tag // des Monats und bezieht dessen Wochentag // von der Instanz var firstDayInstance = new Date(year, month, 1) var firstDay = firstDayInstance.getDay() firstDayInstance = null // Anzahl der Tage im aktuellen Monat var days = getDays(month, year) // Aufruf der Funktion, die den Kalender ausgibt drawCal(firstDay, days, date, monthName, year) } function drawCal(firstDay, lastDate, date, monthName, year) { // Konstante Einstellungen für die Tabelle var headerHeight = 50 // Höhe der Kopfzeile var border = 2 // Rand zwischen den Zellen var cellspacing = 4 // Abstand zwischen den Zellen var headerColor = "midnightblue" // Schriftfarbe der Kopfzeile var headerSize = "+3" // Schriftgröße der Kopfzeile var colWidth = 60 // Breite der Spalten der Tabelle var dayCellHeight = 25 // Höhe der Zellen, in denen die Tage stehen var dayColor = "darkblue" // Schriftfarbe für die Wochentage var cellHeight = 40 // Höhe der Zellen, in denen das Datum steht var todayColor = "red" // Schriftfarbe des heutigen Datums
Listing 5.3: (bsp12.03.htm) Ein Kalender, der auf einer HTML-Tabelle basiert und von JavaScript generiert wird (Forts.)
Exploring JavaScript
149
5 – Zeit und Datum in JavaScript var timeColor = "purple"
// Schriftfarbe der momentanen Zeit
// Anfang der Tabelle. var text = "" // Die Ausgabevariable wird initialisiert. text += '
' text += '
' // table settings text text text text text
+= += += += +=
'
' "" monthName + ' ' + year '' '
'
// Variablen mit konstanten Werten var openCol = '
' openCol += '' var closeCol = '
'
// Ein Array mit den Abkürzungen für die Tage wird erstellt. var weekDay = new Array(7) weekDay[0] = "Mo" weekDay[1] = "Di" weekDay[2] = "Mi" weekDay[3] = "Do" weekDay[4] = "Fr" weekDay[5] = "Sa" weekDay[6] = "So" // Die Kopfzeile der Tabelle wird erstellt, in ihr // stehen die Abkürzungen der Wochentage. text += '
' for (var dayNum = 0; dayNum < 7; ++dayNum) { text += openCol + weekDay[dayNum] + closeCol }
Listing 5.3: (bsp12.03.htm) Ein Kalender, der auf einer HTML-Tabelle basiert und von JavaScript generiert wird (Forts.)
150
Beispiele zu Zeit und Datum text += '
' // Deklaration und Initialisierung von zwei // Hilfsvariablen zur Erstellung der Tabelle var digit = 1 var curCell = 1 for (var row = 1; row <= Math.ceil((lastDate + firstDay - 1) / 7); ++row) { text += '
' for (var col = 1; col <= 7; ++col) { if (digit > lastDate) { break } if (curCell < firstDay) { text += '
' curCell++ } else { if (digit == date) { // Diese Zelle wird für das heutige Datum eingefügt. text += '
' text += '' text += digit text += ' ' text += '' text += '
' + getTime() + '
' text += '' text += '
' } else { text += '
' + digit + '
' } digit++ } } text += '
' } // Die Tabelle wird geschlossen. text += '
' text += '
' // Der gesammelte HTML-Code wird ausgegeben.
Listing 5.3: (bsp12.03.htm) Ein Kalender, der auf einer HTML-Tabelle basiert und von JavaScript generiert wird (Forts.)
Exploring JavaScript
151
5 – Zeit und Datum in JavaScript document.write(text) } // -->
Listing 5.3: (bsp12.03.htm) Ein Kalender, der auf einer HTML-Tabelle basiert und von JavaScript generiert wird (Forts.)
Lassen Sie uns nun das Skript Schritt für Schritt besprechen.
getTime() Diese Funktion gibt einen String mit der aktuellen lokalen Uhrzeit in folgendem Format zurück: Stunden : Minuten Die Funktion basiert auf demselben Algorithmus wie die Funktion setClock() aus Listing 5.1, jedoch ist dies die 24-Stunden-Variante. Falls Sie Verständnis-Probleme hinsichtlich dieser Funktion haben, schauen Sie sich noch einmal Listing 5.1 an. leapYear(year) Diese Funktion gibt true zurück, wenn das übergebene Jahr ein Schaltjahr ist; anderenfalls gibt sie false zurück. Die grundlegende Regel, nach der entschieden wird, ob es sich um ein Schaltjahr handelt, ist die, dass jedes vierte Jahr ein Schaltjahr ist. Ist das Jahr also durch 4 teilbar, muss es ein Schaltjahr sein. Daher eignet sich der Modulo-Operator perfekt für diesen Fall. Wenn year ÷ 4 null ist, dann ist das Jahr durch 4 teilbar und somit ein Schaltjahr. Ansonsten ist das Jahr nicht durch 4 teilbar und false wird zurückgegeben. Der Aufruf dieser Funktion ist: if (leapYear(current year)) // Es ist ein Schaltjahr else // Es ist kein Schaltjahr
152
Beispiele zu Zeit und Datum
Eine andere Möglichkeit wäre, den zurückgegebenen Wert als Bedingung oder in einer Operation (?:) zu benutzen. Beachten Sie, dass als Parameter der Funktion ein Integerwert übergeben werden muss.
getDays(month, year) Diese Funktion nimmt zwei Argumente an, den Monat und das Jahr. Ein Array mit 12 Elementen wird dann erstellt. Das Array ist eine Instanz des internen Array-Objekts. Daher wird das Keyword new benutzt. Jedes Element des Arrays repräsentiert die Zahl der Tage in dem entsprechenden Monat. In ar[0] steht die Zahl der Tage im Januar (31); in ar[11] steht die Zahl der Tage im Dezember. Dem Array werden einfach die richtigen Werte zugewiesen, gemäß den konstanten Anzahlen der Tage in jedem Monat. Allerdings ist die Zahl der Tage im Februar nicht konstant. In Schaltjahren ist der Februar 29 Tage lang, in den anderen Jahren dagegen nur 28. Die Funktion leapYear() wird benutzt, um dies festzustellen. Diese Situation ist typisch für einen Bedingungs-Operator, da der Variablen einer von zwei Werten zugewiesen wird, abhängig vom Wert der Bedingung (dem Booleschen Wert, der von der Funktion leapYear() zurückgegeben wird). Beachten Sie die häufige Verwendung von Kommentaren, die dabei helfen, das Skript zu verstehen. Der Wert, der von der Funktion zurückgegeben wird, entspricht der Anzahl der Tage des Monats, der beim Aufruf übergeben wurde. Falls der Wert von month zum Beispiel 0 war, wird der entsprechende Wert ar[0] == 31 von dieser Funktion zurückgegeben. Beide Argumente müssen Integerwerte sein. Der Monat muss als Integer zwischen 0 und 11 angegeben werden, wobei 0 für Januar und 11 für Dezember steht. getMonthName(month) Diese Funktion erwartet den Integerwert eines bestimmten Monats (0 für Januar, 11 für Dezember) und gibt seinen vollen Namen in Form eines Strings zurück. Sie benutzt ebenso wie die vorherige Funktion eine Instanz des Array-Objekts, um konstante Werte zu speichern. Der Name des gewünschten Monats wird aus dem Array anhand des Indexes ausgelesen.
Exploring JavaScript
153
5 – Zeit und Datum in JavaScript
setCal() Zuerst erstellt diese Funktion eine Instanz des Date-Objekts, in dem die Attribute des aktuellen lokalen Datums gespeichert werden. Das aktuelle Jahr wird der Variablen year mittels der Funktion getYear() zugewiesen, der aktuelle Monat mit der Funktion getMonth(). Der Name des Monats, der von der Funktion getMonthName() zurückgegeben wurde, wird der Variablen monthName zugewiesen. Da die Date-Instanz now nun nicht mehr benötigt wird, setzen wir sie auf null. Die nächste Anweisung der Funktion ist: var firstDayInstance = new Date(year, month, 1)
Sie erstellt eine neue Instanz des Date-Objekts, diesmal für den ersten Tag des aktuellen Monats. Daher wird der Wert 1 für die Datumsangabe verwendet. Dadurch wird natürlich auch der Wochentag des Datums beeinflusst und dieser wird mit der nächsten Anweisung der Variablen firstDay zugewiesen. Die Instanz firstDayInstance wird danach auf null gesetzt, um Ressourcen zu sparen. Dieser Abschnitt des Skripts berechnet den Wochentag, an dem der Monat angefangen hat. Eine andere Möglichkeit, diesen Wert zu ermitteln wäre, eine Instanz des aktuellen Datums wie üblich zu erstellen: var firstDayInstance = new Date() // das ist nicht der erste Tag!
Danach muss man das Datum mit der setDate()-Methode auf 1 setzen. Sie sollten dies mit der folgenden Anweisung tun: firstDayInstance.setDate(1)
Der nächste Teil des Skripts ist nur eine Anweisung lang. Dort wird der Variablen days die Anzahl der Tage des aktuellen Monats zugewiesen. Die letzte Anweisung der Funktion zeichnet den Kalender: drawCal(firstDay, days, date, monthName, year)
Die Argumente sind dabei: 쐌 Der Integerwert des ersten Tags des aktuellen Monats (0 für Montag, 1 für Dienstag usw.) 쐌 Die Zahl der Tage in diesem Monat
154
Beispiele zu Zeit und Datum
쐌 Das heutige Datum (z.B. 11, 23) 쐌 Der Name des aktuellen Monats (z.B. „Januar“, „Februar“) 쐌 Das aktuelle Jahr als vierstellige Zahl (z.B. 2001, 2002)
drawCal(firstDay, lastDate, date, monthName, year) Die Aufgabe dieser Funktion ist es, die Tabelle mit dem Kalender auszugeben. Bevor sie dies macht, muss die HTML-Struktur der Tabelle aufgebaut werden. Im ersten Teil der Funktion werden Variablen Attribute zugewiesen, die das Aussehen und Format der Tabelle bestimmen. Solche Attribute sind zum Beispiel die Größe der Zellen, Farbe der Schrift und Ähnliches. Hier ist eine komplette Liste inklusive der Namen der Variablen und ihren Bedeutungen. Variable
Rolle
headerHeight
Die Höhe der Kopfzeile der Tabelle. Die Kopfzeile ist die Zeile, in der der Name des Monats sowie das Jahr in großer Schrift stehen. Die Höhe wird in Pixeln angegeben.
border
Der Rahmen der Tabelle. Sie wissen bereits, dass Tabellen ein BORDER-Attribut haben. Dieses Attribut verändert die dreidimensionale Höhe des Rahmens.
cellSpacing
Die Breite des Rahmens. Der Abstand zwischen den Zellen einer Tabelle kann ebenfalls in HTML festgelegt werden. Der Wert ist der Abstand zwischen der inneren Linie des Rahmens und der äußeren.
headerColor
Die Farbe der Schrift in der Kopfzeile (Der große Monatsname und das Jahr)
headerSize
Die Größe der Schrift in der Kopfzeile
colWidth
Die Breite der Spalten der Tabelle. Es ist eigentlich die Breite jeder Zelle bzw. die der breitesten Zelle jeder Spalte.
dayCellHeight Die Breite der Zellen, in denen die Namen der Tage stehen
(„Montag“, „Dienstag“, usw.) dayColor
Die Farbe der Schrift für die Wochentage
Tabelle 5.2: Variablen der Funktion drawCal() und ihre Rolle bei der Gestaltung des Kalenders
Exploring JavaScript
155
5 – Zeit und Datum in JavaScript Variable
Rolle
cellHeight
Die Höhe der Zellen, in denen die Daten des gesamten Monats stehen
todayColor
Die Farbe, mit der das aktuelle Datum hervorgehoben wird
timeColor
Die Farbe für die momentane Uhrzeit, die in der Zelle mit dem aktuellen Datum steht
Tabelle 5.2: Variablen der Funktion drawCal() und ihre Rolle bei der Gestaltung des Kalenders (Forts.)
Der folgende Abschnitt des Skripts erstellt die grundlegende Tabellenstruktur in HTML. Beachten Sie, wie die Variablen in dem Skript implementiert wurden. Schauen Sie sich nun die folgenden Anweisungen an: var openCol = '
' openCol += '' var closeCol = '
'
Mit diesen Tags werden die einzelnen Zellen, in denen die Wochentage stehen, erstellt. Zum Beispiel würde man für „Sonntag“ mit den vorgegebenen Werten der Variablen folgende Tags benutzen:
Betrachten wir nun die folgenden beiden Abschnitte der Funktion: // Ein Array mit den Abkürzungen für die Tage wird erstellt. var weekDay = new Array(7) weekDay[0] = "Mo" weekDay[1] = "Di" weekDay[2] = "Mi" weekDay[3] = "Do" weekDay[4] = "Fr" weekDay[5] = "Sa" weekDay[6] = "So" // Die Kopfzeile der Tabelle wird erstellt, in ihr // stehen die Abkürzungen der Wochentage. text += '
' for (var dayNum = 0; dayNum < 7; ++dayNum)
156
Beispiele zu Zeit und Datum { text += openCol + weekDay[dayNum] + closeCol } text += '
'
Im ersten Teil wird ein übliches Array erstellt. Ihm werden dann die Abkürzungen der Wochentage zugewiesen. Dieses Array ermöglicht es uns nun, die Wochentage über eine Zahl anzusprechen. Der nächste Teil, in dem eine Zelle für jeden Tag erstellt wird, benutzt diese Möglichkeit der Bezugnahme. Bei jedem Ablauf der Schleife wird ein neuer Tag erstellt. Beachten Sie, dass die Tags, mit denen die Zeile eingeleitet und abgeschlossen wird, nicht innerhalb der Schleife stehen. Eine neue Zeile für die Wochentage wird vor der Schleife begonnen und nach dem Schleifenablauf für „So“ beendet. Der nächste Teil der Funktion ist: var digit = 1 var curCell = 1
Die Rolle dieser beiden Variablen werden wir im späteren Verlauf der Funktion erkennen. Mittlerweile wurden alle Tags, die sich auf den Tabellenkopf und die Kopfzeile beziehen, zu der Variablen text hinzugefügt. Der restliche Teil der Funktion erweitert sie um die Tags für die Zellen der Tabelle. Wie Sie wissen, ist der Kalender eine rechteckige Tabelle. Daher benutzen wir verschachtelte Schleifen, um auf die Zellen zuzugreifen. Zur Übung können Sie ja mal versuchen, diese Struktur durch eine einzelne Schleife zu ersetzen. Sie können dafür den Modulo-Operator benutzen und durch ihn entscheiden, wann Sie eine neue Zeile anfangen müssen. Der schwierigere Teil der Schleife ist die Endbedingung. Hier ist sie: row <= Math.ceil((lastDate + firstDay – 1) / 7)
Im Moment reicht es zu wissen, dass Math.ceil(num) den nächsten Integerwert, der größer oder gleich num ist, zurückgibt (Aufrunden). Hier sind ein paar Beispiele dazu:
Exploring JavaScript
157
5 – Zeit und Datum in JavaScript Math.ceil(15.15) == 16 Math.ceil(16) == 16 Math.ceil(16.0001) == 17
Wie Sie sich wahrscheinlich erinnern, haben wir bei der vorherigen Funktion, setCal(), festgestellt, dass der Wert, der dem Parameter firstDay übergeben wird, zwischen 1 und 7 und nicht zwischen 0 und 6 liegt. Daher wird in diesem Ausdruck 1 von firstDay abgezogen. Mit Math.ceil((lastDate + firstDay – 1) / 7) wird die Mindestzahl der Zeilen berechnet, die in dem Kalender benötigt wird. Die Zahl der Zellen (abgesehen von Kopfzeile, Spaltenbeschreibungen und der leeren Zelle nach dem letzten Tag des Monats) ist lastDate + firstDay – 1, da lastDate die Anzahl der Tage im Monat ist und firstDate - 1 der Zahl der leeren Zellen vor dem ersten Datum entspricht. Der Wert wird durch 7 geteilt, um die genaue Mindestanzahl der Zeilen zu errechnen. Allerdings muss eine Schleife eine ganze Zahl-mal ausgeführt werden, daher wird die Math.ceil()-Methode benötigt. Einfachere Kalender verwenden einfach fünf Zeilen pro Monat. Allerdings gilt diese einfache Regel mit den fünf Zeilen nicht mehr, wenn (a) der erste Tag eines Februars, der nicht in einem Schaltjahr liegt, auf einen Montag fällt (dann werden nämlich nur vier Zeilen benötigt) und (b), wenn der erste Tag des Monats ein Samstag oder Sonntag ist (was bedeutet, dass sechs Zeilen benötigt werden). Auch wenn sie selten vorkommen, müssen Sie diese Situationen berücksichtigen. Wenn man die Zeilenberechnung durch eine simple Fünf ersetzt, wird zum Beispiel der Monat Februar 1999 oder Februar 2010 nicht richtig dargestellt. Die verschachtelte Schleife ist nicht annähernd so schwierig wie die äußere, sie wird immer siebenmal ausgeführt, für jeden Tag der Woche einmal. Während der gesamten Schleifenkonstruktion, in der inneren und der äußeren Schleife, enthält die Variable digit die aktuelle Zelle, die erstellt wird. Die Variable currCel enthält die wachsende Zahl der bereits erstellten Zellen in der Tabelle. Diese Variable wird nur benötigt, bis der erste Tag eingetragen wurde und wird danach nicht mehr erhöht. Die
- und
-Tags werden zur Erstellung von leeren Zellen benutzt, die als Platzhalter dienen. Sie werden nur vor dem ersten Tag des Monats benötigt, da die Schleife nach dem letzten Tag mit einer break-Anweisung verlassen wird und die restlichen Zellen der Zeile automatisch aufgefüllt werden. Es gibt prinzipiell zwei Arten von Zellen:
158
Beispiele zu Zeit und Datum
쐌 Eine Zelle, die den heutigen Tag darstellt. Dabei wird eine andere Schriftfarbe verwendet und zusätzlich die Uhrzeit ausgegeben. 쐌 Jede andere Zelle Die benutzten HTML-Tags sind offensichtlich und werden hier nicht genauer erklärt. Außerhalb der inneren Schleife aber noch innerhalb der äußeren Schleife stehen zwei weitere Anweisungen. Die erste fängt eine neue Tabellenzeile an, und die zweite beendet sie wieder. Jeder Aufruf der inneren Schleife (entspricht sieben Ausführungen des Anweisungsblocks) ist für die Erstellung einer gesamten Zeile zuständig, ausgenommen, es handelt sich um die letzte Zeile des Monats und der letzte Tag tritt auf, bevor das Zeilenende erreicht wurde. In diesem Fall wird die Zeile nur teilweise erstellt. Mit „Erstellen“ ist eigentlich nur das Hinzufügen des HTML-Codes zur Ausgabevariablen text gemeint. Nun folgt die letzte und wichtigste Anweisung der Funktion. Durch sie wird die gesamte Tabelle, die in der Variablen text gespeichert wurde, in das HTMLDokument ausgegeben und somit der fertige Kalender dargestellt.
5.11.4 Zufallsgenerierte Zitate Zum Abschluss dieses Kapitels wollen wir uns nun noch ein simples, aber dennoch interessantes Skript anschauen, durch das bei jedem Aufruf einer Webseite eine andere Nachricht ausgegeben wird. Hier ist das Skript: <TITLE>Zufallsgenerierte Zitate <SCRIPT LANGUAGE="JavaScript">
Listing 5.4: (bsp12.04.htm) Ein Skript zur Ausgabe von zufälligen Zitaten bei jedem Aufruf der Seite
Exploring JavaScript
159
5 – Zeit und Datum in JavaScript ar[3] = "If there is a possibility of several things going wrong, the one that will cause the most damage will be the one to go wrong." ar[4] = "If there is a worse time for something to go wrong, it will happen then." ar[5] = "If anything simply cannot go wrong, it will anyway." ar[6] = "If you perceive that there are four possible ways in which a procedure can go wrong, and circumvent these, then a fifth way, unprepared for, will promptly develop." ar[7] = "Left to themselves, things tend to go from bad to worse." ar[8] = "If everything seems to be going well, you have obviously overlooked something." ar[9] = "Nature always sides with the hidden flaw." ar[10] = "Mother nature is a bitch." ar[11] = "It is impossible to make anything foolproof because fools are so ingenious." ar[12] = "Whenever you set out to do something, something else must be done first." ar[13] = "Every solution breeds new problems." ar[14] = "Trust everybody ... then cut the cards." ar[15] = "Two wrongs are only the beginning." ar[16] = "If at first you don't succeed, destroy all evidence that you tried." ar[17] = "To succeed in politics, it is often necessary to rise above your principles." ar[18] = "Exceptions prove the rule ... and wreck the budget." ar[19] = "Success always occurs in private, and failure in full view." var now = new Date() var sec = now.getSeconds() alert("Murphy's Law:\r" + ar[sec % 20]) } //-->
Listing 5.4: (bsp12.04.htm) Ein Skript zur Ausgabe von zufälligen Zitaten bei jedem Aufruf der Seite (Forts.)
Die erste Anweisung der Funktion erstellt ein Array, eine Instanz des internen Array-Objekts. Das Array enthält 20 Elemente, es beginnt bei ar[0] und endet bei ar[19]. Jedem Element wird ein String zugewiesen, genauer gesagt eins von Murphys Gesetzen. Danach wird eine Instanz des Date-Objekts namens now erstellt. Die Sekundenzahl wird dann von dieser Instanz mit der Methode 160
Beispiele zu Zeit und Datum getSeconds() bezogen. Wie Sie wissen, liegt der Wert von sec zwischen 0 und 59 mit gleicher Wahrscheinlichkeit für jeden Wert. Insgesamt sind es also 60 aufeinander folgende Integerwerte. Aufgrund dieser Tatsache ergibt der Ausdruck sec ÷ 20 einen Integerwert zwischen 0 und 19, mit einer gleichen Wahrscheinlichkeit für jeden von ihnen, da 60 durch 20 teilbar ist (60 / 20 = 3!). Die Möglichkeit, mit dieser Technik eine Zufallszahl zwischen 0 und 19 zu erstellen, erlaubt es uns, eins von Murphys Gesetzen aus dem Array zufällig auszuwählen. Das gewählte Gesetz wird mit einer Alertbox ausgegeben. Der wichtigste Teil dieses Skripts ist die Benutzung eines Event-Handlers, um auf das load-Event zu reagieren – mit onLoad. Wenn der Event-Handler ausgelöst wird (sobald die Webseite vollständig geladen wurde), ruft er die Funktion getMessage() auf, durch die dann eine solche Meldung ausgegeben wird. Beachten Sie außerdem die Benutzung einer Escape-Sequenz, in der das Zeilenumbruch-Zeichen (\r) verwendet wird.