JavaScript ist eine Sprache mit einem dynamischen Datentypkonzept, die nicht zwischen Objektarten unterscheidet. Obwohl die Sprache an sich recht einfach gehalten ist, ist sie gerade für Programmierer schwer zu durchschauen, die aus dem Bereich der "klassischen" objektorientierten Sprachen kommen. Die Organisation von JavaScript-Anweisungen in Funktionen sieht prozedural aus - ist es aber nicht, wie wir sehen werden. Im ersten Teil dieses Tutorials geht es um die Frage, was ein Objekt in JavaScript ist, welche Objekte uns hier begegnen und wo die Unterschiede zu "normalen" objektorientierten Sprachen liegen. Wir werden uns vordefinierte Objekte anschauen, die zum festen Bestandteil von JavaScript gehören, und erfahren, wie man selbst Objekte definieren kann.
Als Vorkenntnisse setze ich voraus:
Vieles von dem, was wir uns ansehen werden, baut auf dem offiziellen JavaScript-Guide des Mozilla Developer Networks[1] auf. Der Guide ist zwar mit "Core JavaScript 1.5 Guide" überschrieben, enthält aber viele Hinweise zu Neuerungen und Veränderungen der nachfolgenden JavaScript-Versionen. Wer gute Englischkenntnisse, eine Menge Zeit hat - der Leitfaden kommt ausgedruckt auf weit über 100 A4-Seiten - und sich etwas mit der Materie auskennt, kann sich anschließend dort alles noch mal detailiert durcharbeiten.
Für die JavaScript-Eingaben benötigen wir einen modernen Web-Browser wie Firefox, Chrome oder Internet Explorer. Die Anweisungen werden in ein HTML-Dokument eingebettet, in dem vorerst nicht weiter steht als:
<script>
// put your codelines here
var x = ...;
// etc.
</script>
Für unsere Demonstrationen verzichten also auf head und body mit allem, was dazu gehört. Hauptsache, die JavaScript-Anweisungen stehen innerhalb des script-Tags. Um etwas zu sehen, müssen wir dieses Dokument nur noch im Browser öffnen, der das Skript ausführen soll.
JavaScript ist eine objektbasierte Script-Sprache. Das heißt, sie baut auf Objekten auf und benötigt zur Ausführung eine Laufzeitumgebung. JavaScript-Programme laufen in einer "Wirts"-Anwendung. Diese Rolle übernimmt bei uns der Browser. JavaScript selbst ist die Interpretation der Mozilla-Foundation von ECMA-262, einem Industriestandard. Sprachen, die auch auf diesem Standard aufbauen sind z.B. Adobes ActionScript oder Microsofts JScript [2].
Man kann JavaScript-Code schreiben wie man will: irgendwo eine Zeile ins HTML-Markup, hier ein Code-Schnipsel beim Input-Attribut, dort ein paar Zeilen in eine Funktion gepackt, irgendwo ein Variablen definiert - solange alles irgendwie funktioniert. Spaghetti-Code ist schnell geschrieben und schwer zu pflegen. Und das ist der Punkt: eine Anwendung, die in Minuten codiert wurde, läuft womöglich einige Jahre tadellos. Allerdings sind wir keine Elefanten. Nach kurzer Zeit haben wir vergessen, was Zeile XYZ bewerkstelligen soll. Spätere Anpassungen oder Erweiterungen kosten dann jede Menge Nerven. Das gilt für eigene Skripte und erst recht für "geerbten" Code.
Wenn wir JavaScript-Anweisungen konsequent in Objekte verpacken, hat das zunächst den Nachteil, dass wir anfangs mehr nachdenken müssen. Das zahlt sich jedoch später aus. Der Objektansatz ist kein Allheilmittel, aber er hat Vorteile:
Abgesehen davon arbeitet JavaScript intern, wie wir gleich sehen werden, so gut wie immer mit Objekten.
In JavaScript ist im Prinzip ALLES ein Objekt. Sogar Funktionen, allerdings sind diese spezielle Objekte. Eine Objekt-Variable ist eine Variable, der wir mit dem Punkt-Operator neue properties und functions zuweisen können. Auf der konzeptionellen Ebene "normaler" Objektorientierter Programmierung entsprechen die properties den Eigenschaften und die functions den Methoden. Rein prozedural gesehen entsprechen die properties Variablen oder Strukturen bzw. Records und die functions den Subroutinen, also Prozeduren bzw. Funktionen. Die Bezeichnungen werden oft ausgetauscht, wobei die englischen Begriffe, eher technische Züge tragen, und die deutschen die konzeptionelle Ebene beschreiben.
Erhellen wir das mit Anmerkungen zum Sprachaufbau. Grundsätzlich haben wir es in JavaScript also mit folgenden Datentypen zu tun:
Um den Datentyp zu bestimmen, verwenden wir den typeof-Operator. Zum JavaScript-Sprachkern gehören viele vordefinierte Objekte. Zum Beispiel das Array-Objekt als Sammlung von Objekten oder primitiven Datentypen eines Typs, das die Möglichkeiten eines "normalen" Arrays einer Sprache wie C oder Java bei weitem übertrifft. Eine Eigenschaft des Array-Objekts ist die aktuelle Anzahl der Array-Elemente. Ein anderes Objekt, dass stärker auf das Einsatzgebiet von JavaScript abgestimmt ist, ist das Image-Objekt in einem HTML-Dokument. Eine Eigenschaft dieses Bildes ist eine Zeichenkette, die die Quelle der Bilddaten angibt. Die Eigenschaften dieser Objekte können wir ändern: Stecken wir ein neues Element ins Array, ändert sich automatisch die Eigenschaft, die die Anzahl der Elemente des Arrays speichert. Weisen wir der Eigenschaft für die Bilddatenquelle eine andere Zeichenkette zu, wird der Wert der alten Eigenschaft überschrieben. Die Bilddaten werden künftig aus der neuen Quelle geladen – vorausgesetzt, dass die neue Angabe auf eine gültige Bilddatei verweist. Einer primitiven Datentyp-Variable (z.B. boolean, number, string, usw.) können wir KEINE properties und functions zuweisen, selbst wenn sie, wie "string", properties haben.
Welche Eigenschaften und Methoden ein Objekt hat und wie diese zusammenarbeiten, legen wir bei "user-defined object" selbst fest. Es existieren verschiedene Schreibweisen bzw. Notationen, um ganz allgemein neue Objektvariablen anzulegen:
var Object1 = new Object();
var Object2 = [];
var Object3 = {};
window.alert(Object1 is a: "+ typeof Object1); // 'object'
// window.alert('Object2 is ...
Man muss sich nur kurz ins Gedächtnis rufen, wofür das "=" hier steht: nämlich für eine Zuweisung, wobei der Variablen auf der linken Seite der Ausdruck auf der rechten Seite zugewiesen wird. Zwar steht in diesen Beispielen auf der rechten Seite nicht viel, aber es handelt sich in jedem Fall um einen gültigen Ausdruck, der ausreicht, um ein "Objekt-Stummel" im Speicher ins Leben zu rufen. Nachdem wir mit dem Schlüsselwort "var" eine neue Variable angelegt haben, lassen wir uns mit der "window.alert()"-Methode das Resultat von typeof in einem kleinen Konsolenfenster ausgeben, was ich hier angedeutet habe. Bei "window.alert()"(oder auch kurz: "alert()") handelt es sich um eine vordefinierte Methode des window-Objektes. Dieses Objekt steht für das Browserfenster und verfügt beispielweise über die Fähigkeit, kleine Fensterchen über dem originalen Anwendungsfenster zu erzeugen, was man gerne für schnelle Resultat-Ausgaben nutzt. Wenn man die Ausgaben im obigen Beispiel konsequent durchführt, wird deutlich, dass es sich tatsächlich in allen drei Fällen um den Datentyp "object" handelt. An diesem Beispiel sehen wir:
Sobald wir Objektvariablen angelegt haben, können wir sie "erweitern", also mit neuen properties und functions ausrüsten. Beginnen wir mit ersten beiden Objekten:
Object1.prop = "Property with dot syntax";
Object2["prop"] = "Property with hash array syntax";
window.alert(Object1["prop"]+'\n'+Object2.prop);
Ich habe bewusst die Zuweisung und die Ausgabe vertauscht, um die Nähe zwischen einem "hash array" und einem "normalen" Objekt zu unterstreichen. Wir können ein Objekt wie ein Array zugreifen, auch wenn es selbst gar nicht über die Array-Schreibweise entstand. Wir weisen unseren Objekten eine Eigenschaft namens "prop1" zu, die aus einer Zeichenkette besteht. Dass der Name "prop1" für beide Objekte gleich ist, spielt keine Rolle. Es handelt sich ja um zwei verschiedene Objekte, die sich untereinander nicht in die Quere kommen. Anders sähe das aus, wenn eine Eigenschaft mehrfach zugewiesen wird: Dann überschreibt der letzte Eintrag seinen Vorgänger – bei einem hash array muss jeder key einzigartig sein. Wenn wir eine Eigenschaft restlos entfernen möchten, müssen wir den "delete"-Operator gefolgt von der Objekteigenschaft notieren.
Der Begriff "Methode" bezieht sich auf ein Konzpt der Objekt-Orientierten Programmierung und wird praktisch fast immer als Funktion umgesetzt. Das ist bei JavaScript nicht anders. Wir benötigen also für die Zuweisung das Schlüsselwort "function":
Object1.func = function() {
window.alert("I'm a object funct- ahm, a method");
};
Object1.func(); // regular function call
Object2["func"] = function() {
window.alert("I'm a method, too");
};
Object2["func"](); // array-key-like function call
Auch hier haben wir es wieder mit einer Zuweisung zu tun. Wir weisen dem Bezeichner "Object1.func" das zu, was auf der rechten Seite des "=" steht. Nur was steht denn da? Ein schon recht komplexer Ausdruck, der durch das vorangestellte Schlüsselwort "function" zum Funktionsausdruck wird. Die Umbrüche und Einrückungen sind dem Skript selbst übrigens völlig egal, das sieht einfach nur übersichtlicher aus. Folgende Schreiweise wäre absolut korrekt, aber viel schwerer zu lesen:
Object1.func = function(){window.alert("I'm a object funct- ahm, a method");};
Der Ausdruck endet in jedem Fall erst hinter der abschließenden, geschweiften Klammer. Der Zugriff auf unsere Objektmethode erfolgt genau wie bei einer Eigenschaft mit der Punkt-Syntax oder als Array-Schlüssel. Anstatt irgendwo eine Funktion zu definieren, gehen wir also methodisch vor und verknüpfen sie als Methode mit unserer Objektvariablen. Das hat, wie bei den properties, den Effekt, dass auf diese Funktion NUR über das entsprechende Objekt zugegriffen werden kann. Unsere Methode heißt zwar immer gleich, nämlich "func", aber da sie an ein anderes Objekt gebunden ist, spielt das keine Rolle.
Da ein "hash array" eine Sammlung von Datentypen ist, können wir neben den "primitve data types" auch den Code für eine "function" oder ein "object" in einem Hash speichern. Wichtig ist, dass wir beim Aufruf in der Hash-Schreibweise nicht die runden Klammern am Ende vergessen. Die Zuweisung und Ausgabe hätten wir natürlich auch wieder vertauschen können.
Mit der "object literal syntax" können wir das Ganze etwas prägnanter machen:
var Object3 = {
prop : "object3 prop",
func : function() {
window.alert("I'm a function within object literal syntax!");
}
};
Diese Schreibweisen leisten das Gleiche: sie erzeugen ein Objekt. Ich persönlich empfehle die dritte Variante. Zwar sieht die Anhäufung geschweifter Klammern konfus aus, aber wenn man mit Einzügen und Umbrüchen arbeitet, ist es Gewöhnungssache. Die "object literal syntax" zeigt am deutlichsten, dass die Eigenschaft und die Methode zu genau diesem Objekt gehören. Ein Vorteil, der sich aus der kompakten "object literal"-Schreibweise ergibt: es müssen weniger Daten übertragen werden, wodurch die Skripte schneller starten können. Eine ähnliche Schreibweise ist mit der "hash syntax" möglich, aber weil diese typisch für ein Array-Objekt ist, wird sie bei "normalen" Objekten vermieden. Die literale Syntax hat jedoch einen kleinen Haken: Objekt-Variablen können erst NACH ihrer Definition im Script verwendet werden, sonst gibt es eine "undefined"-Fehlermeldung. Das ist nicht weiter verwunderlich, wenn man sich noch mal vor Augen führt, was wir da eigentlich getan haben: wir haben eine Variable angelegt und ihr den Ausdruck auf der rechten Seite zugewiesen. Business as usual, nur dass auf der rechten Seite der Zuweisung anstelle eines "normalen" Wertes ein ziemlich komplexer Ausdruck steht. Man muss auf die Reihenfolge Definition-Verwendung achtgeben. Kein Problem bei einer gut strukturierten Vorgehensweise.
Wer innerhalb eines Objekts, z.B. in einer Methode, auf Eigenschaften dieses Objektes zugreifen möchte, kann diese Eigenschaft ebenfalls nicht einfach beim Namen nennen - das gleiche Spiel wie beim Zugriff an einer beliebigen, anderen Stelle des Skriptes. Der einfache Name würde in diesem Kontext für eine globale Variable gehalten werden. Falls wir in diesem Fall keine Variable unter dieser Bezeichnung haben, gibt es einen Hinweis á la "prop ist not defined", wobei "prop" der Name der Eigenschaft wäre. Falls tatsächlich irgendwo eine globale Variable mit diesem Namen existiert, würden wir uns stundenlang wundern, woher die eigenartigen Werte stammen. Eine fatale Fehlerquelle!
Der Zugriff auf Eigenschaften und Methoden erfolgt grundsätzlich über den Objektnamen gefolgt von einem Punkt und dem, auf das man zugreifen möchte. Gültig wäre folgende Schreibweise:
var Object3 = {
prop : "object3 prop",
func : function() {
window.alert("my prop is: "+Object3.prop);
}
};
Solange wir uns innerhalb eines Objekts befinden, könnten wir statt "Object3.prop" auch "this.prop" schreiben. Allerdings ist diese Vorgehensweise in unserem Fall nicht ratsam. Das Schlüsselwort "this" steht für das Objekt, was gerade aktiv ist. Welches Objekt tatsächlich aktiv ist, hängt vom Kontext ab. Angenommen, wir würden die Methode "Object3.func()" mit einem Ereignis bei einem HTML-Document-Objekt wie einem Link in Verbindung bringen - dann ist das "this" leider nicht mehr unser "Object3", sondern das Link-Objekt. Gerade in komplexen Objekt-Strukturen ist das eine hinterlistige Fehlerquelle. Auch wenn wir innerhalb unseres eigenen Objektes ein anderes Objekt "hineingehen", ändert sich die Bedeutung von "this" automatisch. Es ist oft schwierig festzustellen, wer oder was jetzt "this" ist. Hier sind Entwicklungs-Tools wie die Firefox-Extension "Firebug" [4] eine sehr große Hilfe.
Solange wir es mit einem einzigen Objekt zu tun haben, wirkt der Einsatz eines neuen Schlüsselwortes überzogen. Das "this" wird uns gleich begegnen, wenn wir über das Erzeugen neuer Objekte sprechen. Dort steht "this" für das neu erzeugte Objekt.
Eine Variable der "primitive data types" ("boolean", "number" und "string) erzeugt man, indem einer Variablen ein entsprechender Wert zugewiesen wird, also "true", eine Zahl oder eine Zeichenkette. Das geht aber auch anders:
var boo = new Boolean(true);
var num = new Number(42);
var str = new String("A string object");
Wer die neuen Variablen mit dem typeof-Operator untersucht wird feststellen, dass es sich stets um den Typ "object" handelt. Was ist geschehen? Das Schlüsselwort "new" erzeugt ein neues Objekt im Speicher, wie in anderen Sprachen auch. Danach kommt ein Bezeichner, wobei der Großbuchstabe zu Beginn des Namens auf ein Objekt hinweist, und runde Klammern, in denen als Argument steht, was wir sonst bei den "primitive data types" unseren Variablen zugewiesen haben. Es handelt sich um einen Funktionsaufruf, der ein Objekt zurück liefert. Das erledigt unter der Haube eine so genannte "constructor"-Funktion. Ihr Rückgabewert ist ein neues Objekt, "instance" oder zu gut deutsch: "Instanz", genannt. Das funktioniert ähnlich wie in "normalen" objektorientierten Sprachen, nur dass es keine explizite Vorlage im Sinne einer Klassendefinition für Instanzen gibt, sondern einen "constructor", der ein neues Objekt erzeugt. Hier erzeugen nicht Klassen Instanzen, sondern Objekte erzeugen Objekte. Darum ist JavaScript eine objekbasierte UND klassenlose Sprache. Der erste Buchstabe des "constructors" wird per Konvention groß geschrieben. Diese Art von Objekten nennt man auch "Wrapper", d.h. "Hüllen", die einen einfachen Wert in ein Objekt einpacken.
Jedes neue Objekt, das so erzeugt wurde, erhält automatisch die "constructor"-Eigenschaft:
var string_type = "I am a primitive String";
var string_object = new String(""I am a String-object"");
window.alert('string_type is a '+typeof string_type+'\nstring_type.constructor : '+str.constructor+'\nstring_object is a: '+typeof string_object+'\nstring_object.constructor : '+string_object.constructor);
An dieser Stelle drängt sich die Frage auf, warum man eine Zahl oder Zeichenkette mal so und mal so erzeugen kann und welche Vorgehensweise die bessere ist. Normalerweise reicht es, über eine einfache Zuweisung einen einfachen Datentyp zu verwenden. Falls wir aber zusätzliche Funktionalitäten oder Daten fest mit unseren Objekt-Variablen verknüpfen wollen, oder Vorhandene überschreiben möchten, brauchen wir die Objekt-Versionen. Wir erinnern uns: einem "object" können wir properties und functions zuweisen, einem "string" dagegen nicht.
Der "constructor" ist ein Objekt. Rein technisch gesehen speichert der constructor lediglich seinen eigenen Sourcecode in einer neuen Objekt-Variable ab, die dann unabhängig vom Original existiert. Die Ausgabe "[native code]" bedeutet, dass wir es mit einem vordefinierten "constructor" des JavaScript-Sprachkerns zu tun haben. Hier wird uns der Code leider nicht angezeigt, doch bei einem selbst definierten "constructor" stünde hier alles, was zwischen den geschweiften Klammern folgt. Der Aufruf "constructor()" entspricht übrigens exakt dem ursprünglichen Aufruf, mit dem wir die Objektvariable erzeugt haben. Da wir Funktionen wie Objekte behandeln dürfen, könnten wir so z.B. eine Konstruktor-Funktion auch in einem anderen Kontext verwenden.
Wie wir sehen, hat der einfache String den gleichen "constructor" wie die String-Instance. Wenn wir wissen möchten, wie die entsprechende Objekt-Variable entstanden ist, kommen wir mit dem typeof-Operator nicht weiter. Für solche Unterscheidungen gibt es, wie in Java, den "instanceof"-Operator. Man befragt eine Objekt-Variable nach ihrem mutmaßlichen Objekt-Typ und er meldet "true" oder "false" zurück:
if(string_object instanceof String == true)
window.alert('string_object is an string instance');
if(string_type instanceof String == false)
window.alert('string_type is no string instance');
Der einfache String ist also keine Instanz des JavaScript-String-Objekts.
Nehmen wir an, wir möchten ein Muster-Objekt definieren, von dem wir neue Objekte bzw. Instanzen erzeugen können – also so etwas wie eine Klasse mit einer Konstruktormethode in der Objektorientierten Programmierung. Jede Instanz soll eine andere Zeichenkette im Browser-Fenster ausgeben. Welche das ist, soll neuen Objekten bei ihrer Erzeugung mit auf den Weg gegeben werden. Nach dem, was wir gesehen haben, würden wir ein Objekt definieren und dann, wie beim String-Beispiel, mit dem Schlüsselwort "new" eine Instanz erzeugen. Das folgende Beispiel baut auf dem auf, was wir zur "object literal syntax" und den Ausdrücken besprochen hatten. Es funktioniert nur leider nicht wie gewünscht:
var Message = {
message : "test",
getMessage : function () {
window.alert('Message: '+this.message);
}
};
// try to create a new instance
var myMessage = new Message("Hello World!");
myMessage.getMessage();
Firebug würde uns melden: "Message is not a constructor". Wir müssen unser Message-Objekt in einen Message-"constructor" verwandeln - aber was ist das überhaupt? Der "constructor" ist eine Funktion ist, d.h. wir müssen aus unserem Einzelobjekt ein Funktionsobjekt machen. Wenn wir das Schlüsselwort "function", gefolgt von runden Klammern, nach der ersten Zuweisung einfügen, sind wir auf dem richtigen Weg. Nur merken wir leider nichts davon, weil Firebug jetzt ein 'invalid label' in der Zeile mit der "getMessage"-Funktion bemängelt, was auf einen Sytaxfehler hindeutet. So kommen wir nicht weiter. Außerdem ist noch offen, wie die neuen Instanzen an ihren Message-Text kommen. Wir müssen ja in unserer Definition irgendwo den Text übergeben. Genrell müssen wir uns ein Stück weit von der Syntax verabschieben, mit der wir bislang einzelne Objekte erzeugt haben. Das fertige Resultat könnte so aussehen:
var Message = function (mess) {
this.message = mess,
this.getMessage = function () {
window.alert('Message: '+this.message);
}
};
// create new instance
var myMessage = new Message("Hello World!");
myMessage.getMessage();
Aus den Doppelpunkten ist eine Zuweisung geworden. Wir definieren kein Objekt in "sprichwörtlicher Objekt Syntax", sondern eine "constructor-function", also eine Funktion, die beim Aufruf ein neues Objekt erzeugt. Genauer gesagt definieren wird ein Funktionsobjekt als Funktionsausdruck, d.h. als Ausdruck, der wie eine einzelne Programmzeile ausgewertet wird - man beachte das Semikolon hinter der 2.geschweiften Klammer, die uns das Ende der Zeile anzeigt, die mit "var Message ..." startete. Ebenso handelt es sich bei der Methode "getMessage" um einen Bezeichner, dem wir den Funktionsausdruck "function() {}" zuweisen.
Das Verhältnis zwischen Funktion, Objekt, Array und Klasse in JavaScript ist gewöhnungsbedürftig, wenn man eine strikte Trennung zwischen diesen Konzepten gewohnt ist. Grundsätzlich ist ein Objekt ein assoziatives Array. Eine Funktion ist ein Objekt, aber nicht jedes Objekt ist eine Funktion:
[1] Mozilla Core JavaScript: https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide
[2] Wikipedia-Eintrag zu JavaScript: http://de.wikipedia.org/wiki/JavaScript
[3] Wikipedia-Eintrag zu JSON: http://de.wikipedia.org/wiki/JSON
[4] Homepage Firebug: http://getfirebug.com/
Veröffentlicht auf BITSRC.info unter URL http://bitsrc.info/development_js_ob_I.php (Letzte Änderung: 07-06-2010, 13:18:38.)
Module | Development | Tools | Impressum |
---|---|---|---|
Modulpläne alte Materialien |
JavaScript |
Firefox und Extensions MinGW - Minimal GNU for Windows Serverumgebungen |
Impressum Haftungsausschluss |
2007-2025 Uwe Hartwig (Ausgabe in
0,0004 sec erzeugt)