Teil von SELFHTML aktuell Teil von Artikel Teil von JavaScript

Objekt-Handling in JavaScript

nach unten Christian Kruse
nach unten Einleitung
nach unten Die Implementation von Objekten
nach unten Interne Repräsentation von Objekten
nach unten Variablen-Objekt
nach unten Vererbung in JavaScript
nach unten Praktischer Nutzen
nach unten Weiterführende Literatur

Christian Kruse

E-Mail: E-Mail ckruse@wwwtech.de
Homepage-URL: deutschsprachige Seite http://wwwtech.de/

Bei Fragen zu diesem Beitrag bitte den Autor des Beitrags kontaktieren!

nach obennach unten

Einleitung

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.

nach obennach unten

Die Implementation von Objekten

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:

Popup-Seite Anzeigebeispiel: So sieht's aus

   function func() {
   }

   alert(typeof(func));
  

Der Standard sagt dazu, dass Funktions-Definitionen wie folgt abgearbeitet werden:

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:

Popup-Seite 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:

Popup-Seite 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:

Popup-Seite 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:

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

nach obennach unten

Interne Repräsentation von Objekten

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:

Popup-Seite 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:

  1. Der Punkt-Operator ist (fast) äquivalent zum []-Operator.
  2. Man kann einem JavaScript-Objekt nach seiner Instanziierung neue Eigenschaften und Methoden zuweisen.
  3. Man muss nicht den Punkt-Operator benutzen, um auf Eigenschaften oder Methoden zuzugreifen.
  4. Man kann vorhandene Methoden (Attribute sowieso) überschreiben

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:

Popup-Seite 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:

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

nach obennach unten

Variablen

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):

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

nach obennach unten

Vererbung in JavaScript

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:

Popup-Seite 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:

Popup-Seite 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:

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

nach obennach unten

Praktischer Nutzen

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.

nach obennach unten

Weiterführende Literatur

Teil von SELFHTML aktuell Teil von Artikel Teil von JavaScript

© 2007 bereichsübergreifende Seite Impressum