| E-Mail: | |
|---|---|
| Homepage-URL: |
Bei Fragen zu diesem Beitrag bitte den Autor des Beitrags kontaktieren!
Immer wieder fällt mir auf, dass Leute Probleme mit dem Objekt-Handling in JavaScript haben. Das äußert
sich meist darin, dass sie wilde eval()-Konstruktionen einsetzen, obwohl es eigentlich gar nicht nötig
ist. Und um genau solche Verbrechen[tm] zu vermeiden, habe ich diesen Artikel geschrieben. Ich werde hier nicht auf die
verschiedenen DOMs (Document Object Models) eingehen. Wer daran Interesse hat, möge sich bitte bei Microsoft,
Netscape und beim W3C umsehen. Ich möchte viel allgemeiner an die Sache herangehen und die Sprach-Umsetzung
erklären.
Ich weiß, einige werden mich jetzt steinigen, aber ich muss das jetzt mal ganz deutlich sagen: JavaScript ist objekt-orientiert. Selbst fortschrittliche OO-Patterns sind damit möglich. Allerdings basiert das Objekt-Modell von JavaScript nicht auf Klassen, sondern auf Prototypen. Das heißt, es gibt keine Klassen, sondern nur Objekte. Wenn man z. B. eine Funktion definiert, so wird implizit mit der Funktion auch direkt ein Objekt angelegt. Das kann man an dem folgenden Beispiel sehr schön sehen:
Anzeigebeispiel: So sieht's aus
function func() {
}
alert(typeof(func));
Der Standard sagt dazu, dass Funktions-Definitionen wie folgt abgearbeitet werden:
undefined.
Wird eine Funktion ausgeführt, so wird für jeden formalen Parameter (in JavaScript müssen
Parameter nicht zwingend formal deklariert werden) das Attribut im Funktions-Objekt auf den Wert des entsprechenden
Parameters gesetzt. Ist die Funktion jedoch zu Ende gelaufen, wird das Attribut wieder auf undefined gesetzt:
Anzeigebeispiel: So sieht's aus
function x(z) {
alert(x.z);
}
x("calling x");
alert(x.z);
Doch nicht nur Funktions-Definitionen erzeugen Objekte. Auch Variablen erzeugen Objekte. Was genau für ein Objekt erstellt wird, hängt vom Kontext ab:
Anzeigebeispiel: So sieht's aus
var und; var str = "string"; var num = 10; var obj = new Number(10.1); alert(typeof(und)+" "+typeof(str)+" "+typeof(num)+" "+typeof(obj)); und = 50; alert(typeof(und));
In diesem Beispiel werden zuerst vier Objekte erzeugt: ein String-Objekt, ein Number-Objekt, und ein
allgemeines Objekt. Der Variablen und ist noch kein Objekts-Typ zugewiesen. Erst durch die Zuweisung
und = 50; wird dieser Variablen implizit ein Objekts-Typ zugewiesen.
Die Tatsache, dass Konstruktoren auch schon Objekte sind, bedeutet, dass neue Objekt-Instanzen durch Objekte initialisiert werden, von "prototypischen Objekten". Da diese Konstruktoren und Methoden eben auch Objekte sind, kann eine beliebige Code-Sequenz darin ablaufen. Das heißt, man kann im Kontext entscheiden, ob ein objektabhängiger Code oder etwas vollkommen anderes ausgeführt werden soll:
Anzeigebeispiel: So sieht's aus
function dhtmllib() {
this.setxy = setxy;
}
function setxy(x,y,obj) {
if(this == self) {
alert("called to handle obj from the parameter list (obj is a reference to an html element)");
}
else {
alert("called to handle this (this could describe a html element)");
}
}
dhtml = new dhtmllib();
dhtml.setxy(10,10);
setxy(10,10,document.getElementById("div1"));
Das ist weder in C++, noch in Java so möglich. In Java und C++ werden Klassen definiert, und Objekte werden aus
ihnen heraus erzeugt. Es ist (abgesehen von statischen Methoden) nicht möglich, Methoden oder Attribute aufzurufen,
ohne vorher ein Objekt zu instanzieren.
Praktisch unterscheidet sich ein Funktions-Aufruf von einem Methoden-Aufruf nur dadurch, dass this auf ein
anderes Objekt referenziert. In einem Funktions-Aufruf referenziert this auf das aktuelle
window-Objekt, in einem Methoden-Aufruf auf das Objekt, dessen Methode gerufen wurde. Das ist eigentlich auch
sehr logisch, da alle Funktionen und Variablen, wie bereits erklärt, Attribute des aktuellen
window-Objekts sind:
Anzeigebeispiel: So sieht's aus
function clss() {
this.func = func;
}
function func() {
alert(this.attribute);
}
attribute = "value";
func();
obj = new clss();
obj.func();
Im oberen Beispiel wird klar, dass this sich immer auf das aktuelle Objekt bezieht.
Viel wichtiger ist aber, wie genau JavaScript-Objekte intern dargestellt werden: JavaScript-Objekte sind assoziative Arrays. Das heißt, dass ein JS-Objekt nichts anderes ist als ein Array, dessen Attribute und Methoden als Array-Elemente eingetragen sind. Diese Aussagen können wir sehr leicht verifizieren:
Anzeigebeispiel: So sieht's aus
function display() {
alert(this.name);
}
function myclass() {
this.name = "value";
this.display = display;
}
obj = new myclass();
for(n in obj) {
alert("Name: "+n+", Typ: "+typeof(n));
}
Als Ausgabe werden sie zwei JavaScript-Popups mit dem Namen und dem Typ des Array-Eintrags sehen. Wie man hier sieht, gibt es zwei Elemente in dem Objekt: 'name' und 'display'. Beide sind vom Typ 'string', da Sie Array-Keys darstellen, über die man auf die Objekts-Eigenschaften und -Methoden zugreifen kann. Daraus ergeben sich vier wichtige Erkenntnisse:
[]-Operator.
Wie Sie sicher bemerkt haben, habe ich oben geschrieben, der Punkt-Operator sei fast äquivalent zum
[]-Operator. Das stimmt, denn mit dem Punkt-Operator kann man keine numerischen Indizes ansprechen.
Außerdem ist das Argument für den Punkt-Operator ein Ausdruck, mit allen Einschränkungen (dazu
gehören auch die durch die Sprachdefinition gegebenen Namens-Regelungen). Das Argument für den
[]-Operator ist dagegen ein String und unterliegt damit auch nicht den Namenskonventionen in JS.
Das heißt, über den []-Operator kann man auch Objekt-Attribute anlegen, auf die man mit dem
Punkt-Operator nie zugreifen könnte:
Anzeigebeispiel: So sieht's aus
function myclass() {
this["Attribut-Name mit Leerzeichen"] = "abc";
}
obj = new myclass();
alert(obj["Attribut-Name mit Leerzeichen"]);
Diese Tatsache bietet viele Vorteile. So kann man z. B. endlich trotz der durch PHP gegebenen Namens-Konventionen für Formular-Elemente auf diese zugreifen:
Anzeigebeispiel: So sieht's aus
<html>
<head>
<title>Testseite</title>
<script type="application/x-javascript">
function cllback() {
var frm = document.forms.testform;
for(var i=0;i<frm.elements['multival[]'].length;i++) {
alert(document.forms['testform'].elements['multival[]'][i].value);
}
}
</script>
</head>
<body>
<form method="GET" action="url" name="testform">
<input type="text" name="multival[]">
<input type="text" name="multival[]">
<input type="button" value="Werte anzeigen!" onclick="cllback()">
</form>
</body>
</html>
Zur Erklärung: damit bei doppelten Feld-Namen alle Werte korrekt in PHP verfügbar sind, muss man entweder einen Array-Index in den eckigen Klammern stehen haben oder zwei leere Klammern hinter den Feld-Namen schreiben. Ansonsten ist nur der letzte Wert verfügbar.
Die einzige Notwendigkeit, mit der sich eval()-Benutzer jetzt noch rechtfertigen könnten, ist die
Tatsache, dass man manchmal Variablen-Namen als String vorliegen hat. Doch auch hier muss ich einen Strich durch
die Rechnung machen: in JavaScript wird jedes Variablen-Objekt und jedes Funktions-Objekt als Attribut in
einem Variablen-Objekt registriert. Was das Variablen-Objekt ist, hängt vom Kontext ab. Bei globalen
Variablen und Parameter ist es das aktuelle window-Objekt (in self referenziert):
Anzeigebeispiel: So sieht's aus
var globvar = "val";
function func() {
alert("func called!");
}
function myclass() {
this.func = func;
}
alert("Variable globvar: " + self["globvar"]);
for(entry in self) {
alert(entry);
}
Ich habe hier self genommen, weil bei Framesets das window-Objekt das übergeordnete wäre,
und nicht das des aktuellen Frames. Zur Verdeutlichung: angenommen, sie haben ein Frameset. Dann haben sie
ein window-Objekt, dem ein Array von window-Objekten untergeordnet ist: für jeden Frame
ein Eintrag. Haben Sie also zwei Frames, so hat das frames-Attribut des window-Objekts auch zwei
Einträge. Und da jeder Frame einen eigenen Namensraum hat, werden die Funktionen und Variablen des Frames auch
in dem dortigen window-Objekt eingetragen. Deshalb sollte man immer self benutzen, wenn man sich
auf das aktuelle Fenster bezieht.
Tatsächlich ist es übrigens so, dass Funktionen und Variablen nur in den Variablen-Objekten
referenziert werden. Wenn Sie z. B. eine Funktion oder eine Variable direkt referenzieren und nicht als Attribut,
dann wird die Scope Chain abgearbeitet: zuerst wird geschaut, ob man in einem with- oder
catch-Block ist und die Variable dort registriert ist. Danach wird geschaut, ob man sich gerade in einer
Funktion befindet. Wenn ja, wird dort das referenzierte Objekt gesucht. Danach geht es in einen eventuellen
Objekts-Kontext. Auch dort wird nach einem Attribut gesucht. Danach werden die Super-Klassen der Reihe nach durchlaufen.
Schließlich wird im aktuellen window-Objekt gesucht. Und zuletzt wird im obersten window-Objekt
gesucht. So wird z. B. ein Funktions-Aufruf alert(); zuerst expandiert zu self.alert();, und danach
zu window.alert();, da alert() eine Builtin-Funktion ist und im obersten window-Objekt
registriert wird.
Vererbung in JavaScript geschieht (wie könnte es anders sein) über den Prototypen. Der Prototyp kann
über das "prototypische Objekt" durch das Attribut prototype referenziert werden:
Anzeigebeispiel: So sieht's aus
function proto() {
}
alert(proto.prototype);
Über diese Referenz kann man Attribute und Methoden an das prototypische Objekt und alle davon abgeleiteten Instanzen verteilen, ganz einfach, indem man eine entsprechende Zuweisung macht:
Anzeigebeispiel: So sieht's aus
function proto() {
}
oproto = new proto();
proto.prototype.sinn_des_lebens = 42;
for(attr in oproto) {
alert(attr+": "+oproto[attr]);
}
Mit der obigen Anweisung haben das prototypische Objekt, alle zukünftig instanzierten Objekte und alle bereits
instanzierten Objekte die Eigenschaft sinn_des_lebens mit dem Wert 42. Genau so kann man das natürlich
mit Methoden machen. Möchte man jedoch ein komplettes prototypisches Objekt erben, so würde man das etwa
so machen:
Anzeigebeispiel: So sieht's aus
function sper() {
this.attr = "val";
}
function inherited() {
}
inherited.prototype = new sper();
x = new inherited();
for(attr in x) {
alert(attr+": "+x[attr]);
}
Das Prototypen-Objekt stellt also ein sehr mächtiges Mittel zur Verfügung.
Der praktische Nutzen dieser Informationen ist vielfältig. Zum Beispiel ist es nun (bedingt) möglich, das
W3C-DOM für ältere Browser nachzubauen und so den notwendigen Code auf ein Minimum zu reduzieren. Die
getElementById-Methode könnte man für den IE4 und den NN4.x z. B. so nachbauen:
function get_element_by_id_ie4(id) {
if(document.all[id]) {
document.all[id].getElementById = get_element_by_id_ie4;
}
return document.all[id];
}
function get_element_by_id_nn4(id) {
var base = this;
if(this.document) {
base = this.document;
}
if(base.layers && base.layers[id]) {
base.layers[id].getElementById = get_element_by_id_nn4;
return base.layers[id];
}
else {
if(base.forms && base.forms[id]) {
base.forms[id].getElementById = get_element_by_id_nn4;
return base.forms[id];
}
else {
if(base.images && base.images[id]) {
base.images[id].getElementById = get_element_by_id_nn4;
return base.images[id];
}
else {
if(base.applets && base.applets[id]) {
base.embeds[id].getElementById = get_element_by_id_nn4;
return base.embeds[id];
}
else {
if(base.links && base.links[id]) {
base.links[id].getElementById = get_element_by_id_nn4;
return base.links[id];
}
else {
if(base.anchors && base.anchors[id]) {
base.anchors[id].getElementById = get_element_by_id_nn4;
return base.anchors[id];
}
else {
if(base.embeds && base.embeds[id]) {
base.embeds[id].getElementById = get_element_by_id_nn4;
return base.embeds[id];
}
}
}
}
}
}
}
return undefined;
}
if(document.layers) {
document.getElementById = get_element_by_id_nn4;
}
else if(document.all && !document.getElementById && !window.opera) {
document.getElementById = get_element_by_id_ie4;
}
Natürlich befreit das nicht zu 100% von den Browser-Klippen, aber es vereinfacht das Handling durchaus.