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 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
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 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.