Benutzer:Rolf b/tempObjekteEigenschaften

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

In Arbeit / Inkonsistent!

Ein Objekt im Sinn der Objektorientierten Programmierung ist eine Datenstruktur, die eine definierte Menge von Variablen und Operationen auf diesen Variablen vereint. Die Definition, was Teil dieser Menge ist, nennt sich Klasse. JavaScript bietet Sprachkonstrukte, mit denen man Klassen und Objekte realisieren kann, die aber über die starren Möglichkeiten von OO-Sprachen wie C++ oder Java hinausgehen.

Objekte können mit Gegenständen im wirklichen Leben verglichen werden. So wie ein Auto anhand seiner Eigenschaften wie Marke, Farbe, Baujahr und TÜV-Zulassung beschrieben wird, können Objekte Eigenschaften (properties) haben, die die Merkmale beschreiben. Und so, wie ein Auto bestimmte Dinge tun kann, kann ein Objekt Funktionen enthalten, die an Hand der Objekteigenschaften Aktionen durchführen.

In JavaScript ist ein Objekt ein flexibler Container, in dem man unter beliebig wählbaren Namen Werte speichern und abrufen kann. Diese Name-Wert Paare nennt man die Eigenschaften dieses Objekts. Eine Festlegung auf eine Klasse ist nicht erforderlich. Es ist allerdings möglich, das Konzept der Klasse in JavaScript zu realisieren. JavaScript bringt etliche vordefinierte Objekte mit, die als Klassen genutzt werden können.

Grundsätzliches

Erzeugen eines Objekts

Um ein leeres Objekt zu erzeugen, kann man den new Operator und die vordefinierte Objektklasse Object verwenden. Alternativ gibt es eine spezielle Schreibweise, um Objekte direkt aufzuschreiben. Beides führt zum gleichen Ergebnis:

Beispiel
var objekt = new Object();
var leer = { };

Die Klammern bei new Object() sind in JavaScript optional. Relevant sind sie beim Einsatz von Konstruktor-Funktionen, auf die später eingegangen wird.

Zugriff auf Eigenschaften von Objekten

Um einem Objekt Eigenschaften zu geben, kann man sie ihm einfach zuweisen. Als Name für eine Objekteigenschaft oder -methode kann eine beliebige Zeichenkette gewählt werden. Solange der Name den Regeln für Variablennamen in JavaScript folgt, kann der Punkt-Operator für den Zugriff genutzt werden.

Beispiel
var frage = {};                          // Objekt erzeugen
frage.text = "Was ist 7+7";              // Erzeugt die Eigenschaft "text" mit dem Wert "Was ist 7+7".

                                         // Liest die Eigenschaft "text" und speichert den Wert
                                         // als Text eines DOM Elements
document.getElementById("frage").innerText = frage.text;

Namen von Eigenschaften sind nicht auf gültige Variablennamen beschränkt. Um solche Namen zu nutzen, muss man den auch für Arrays genutzen Zugriffsoperator verwenden.

Beachten Sie, dass allein das Zuweisen an einen numerischen Index das Objekt noch nicht zu einem Array macht!
Beispiel
var frage = {};                          // Objekt erzeugen
frage["Wer bin ich?"] = "Ein Quiz";      // Erzeugt Eigenschaft mit Name "Wer bin ich?"
frage[7] = 123;                          // Erzeugt eine Eigenschaft mit Name "7" und dem Wert 123
console.log(frage.length);               // -> undefined!!! frage ist kein Array.
console.log(frage["Wer bin ich?"]);      // -> Ein Quiz

Nützlich ist der [] Operator nicht nur bei beliebige Namen für Eigenschaften, sondern vor allem dann, wenn der Name der Eigenschaft nicht konstant ist, sondern Inhalt einer anderen Variablen ist.

Entfernen von Eigenschaften

Der JavaScript Operator delete entfernt Eigenschaften aus Objekten, die nicht mehr benötigt werden. Wichtiger als im hier gezeigten trivialen Beispiel ist delete beim Einsatz von Objekten als Datenstrukturen (siehe unten).

Beispiel
var frage = { ... };                     // Frage von oben
delete frage["Wer bin ich?"];            // Weiß ich doch eh.

Objektliterale

Die Erzeugung eines Objekts und das Befüllen mit Eigenschaften lässt sich verbinden. Dazu existiert in JavaScript eine besondere Syntax, das Objektliteral. Es wird oft auch "Objektinitialisierer" genannt, das ist aber etwas irreführend, denn Objektliterale können auch für sich stehen (ein Beispiel folgt weiter unten).

Beispiel
var frage = {
   text: 'Was ist 7+7?',
   antwort: 14
};
// Es geht auch in einer Zeile:
var frage2 = {text:'Die Antwort auf DIE Frage?', antwort: 42};

Ein Objektliteral wird von geschweiften Klammern eingeschlossen, innerhalb derer die gewünschten Eigenschaften durch Kommata getrennt aufgelistet werden. Eine Eigenschaft wird durch ihren Namen, einen Doppelpunkt und einen Wert festgelegt. Wenn ein Name kein gültiger JavaScript-Bezeichner ist, muss man ihn in Anführungszeichen (oder Hochkommata) setzen. Der Wert rechts vom Doppelpunkt ist beliebig, es kann eine Konstante sein, ein Ausdruck oder sogar ein Array- oder Objektliteral. Links vom Doppelpunkt ist es so, dass bis ECMAScript 5 nur einfache Namen zugelassen waren. Die mit ECMAScript neu hinzugekommenen Möglichkeiten werden im nächsten Abschnitt separat beschrieben.

Beispiel
var serie = 'Star Trek',
    falsch = [ 32, 42 ],
    frage = {
       text: 'Welche Zahl verfolgt einen '+ serie +' Zuschauer?',
       antworten: Array.concat(falsch, 47);
    };
    // frage ist jetzt { text: 'Welche Zahl verfolgt einen Star Trek Zuschauer', antworten: [ 32, 42, 47 ] }
Beachten Sie: Ein Objektliteral erzeugt immer ein neues Objekt. Wenn Sie mit dem gleichen Literal zweimal ein Objekt erzeugen, sind diese Objekte nachher nicht identisch. Variablen, die in einem Objektliteral stehen, werden beim Verarbeiten des Literals mit ihrem aktuellen Inhalt ausgewertet.

ECMAScript 6: Abkürzende Schreibweisen und berechnete Eigenschaftsnamen

Mit ECMAScript 6 sind einige weitere Möglichkeiten hinzugekommen, Objektliterale aufzuschreiben. Sie werden von den größeren Browsern mittlerweile unterstützt (außer Internet Explorer).

  • Eigenschaftsnamen können berechnet werden. Dazu schreibt man den Ausdruck, der den Eigenschaftsnamen liefert, in eckige Klammern. Dieser Ausdruck ist frei konstruierbar, er kann auch seinerseits wieder Funktionen aufrufen.
  • Da Konstrukte wie obj = { text: text, antwort: antwort } häufig vorkommen, um eine Menge von Variablen zu einem Objekt zu bündeln, kann man das durch obj = { text, antwort } abkürzen.
Beispiel
   var text = 'Was ist 7+7?',
       antwort = 42,
       i = 0,
       frage = {
          text, antwort,
          ["punkte " + ++i]: 5*i,
          ["punkte " + ++i]: 5*i,
          ["punkte " + ++i]: 5*i
       };
       // Erzeugt { text: "Was ist 7+7?", antwort: 42, 'punkte 1': 5, 'punkte 2': 10, 'punkte 3': 15 }

Die Punktetabelle würde man normalerweise mit einem Array aufbauen, hier geht es nur um die Demonstration dieses ECMAScript 6 Features.

Objekte sind keine skalaren Werte

Die Formulierung: "Ich speichere das Objekt in einer Variablen" ist gängig, aber falsch. Richtig ist, dass man einen Verweis auf dieses Objekt in einer Variablen speichert. Und wenn man den Inhalt dieser Variablen an eine andere Variable zuweist, dann kopiert man den Verweis, und nicht das Objekt.

Beispiel
   var kardinal = { name: 'Ratzinger', vorname: 'Josef' };  // Ein Kardinal aus Freising
   var papst = kardinal;                                    // wird zum Papst
   papst.name = 'Bergoglio';                                // und bekommt einen Nachfolger

   console.log("Der Kardinal aus Freising heißt " + kardinal.vorname + " " + kardinal.name);
   // Der Kardinal aus Freising heißt Josef Bergoglio ?!

Methoden

Um einem Objekt nicht nur Informationen, sondern auch Verhalten zuordnen zu können, gibt es eine besondere Kategorie von Eigenschaften: die Methoden. In JavaScript handelt es sich dabei um Eigenschaften, deren Wert eine Funktion ist und die genau wie globale Funktionen aufrufbar sind.

Beispiel
var frage = {
       text: 'Was ist 7+7?', 
       antwort: 14,
       _punkte: [ 15, 10, 5 ],
       _richtig: false,
       anzVersuche: 0,
       prüfeAntwort: function(versuch) {               // <--- Methode 1
          if (!this._richtig)
          {
             this.anzVersuche++;
             !this._richtig = (versuch == this.antwort);
          }
          return this._richtig;
       },
       punkte: function() {                            // <--- Methode 2
          if (!this._richtig || this.anzVersuche > this._punkte.length)
             return 0;
          else
             return this._punkte[this.anzVersuche-1];
       },
       istGelöst: function() {                         // <--- Methode 3
          return this._richtig;
       }
    };

var punkteAktuell = frage.istGelöst();   // -> false

Unsere Frage hat eine Eigenschaft _punkte bekommen, die ein Array mit der Punktzahl für eine richtige Antwort im ersten, zweiten oder dritten Versuch liefert. Eine weitere Eigenschaft anzVersuche soll zählen, wie oft der Kandidat eine Antwort versucht hat, und ein Kennzeichen _richtig gibt an, ob die Antwort gewusst wurde. Die _richtig Eigenschaft wurde mit einem vorangestellten Unterstrich benannt, was eine beliebte Konvention ist, um anzuzeigen, dass dies eine interne Objekteigenschaft ist und von den Nutzern des Objekts nicht direkt verwendet werden soll. Es ist aber auch nur das: Ein Schild "Bitte nicht berühren", das jeder ignorieren kann.

Drei weitere Eigenschaften bekommen als Wert eine anonyme Funktion und werden damit zu Methoden. Eine Besonderheit in Methoden ist die Variable this. Wird eine Funktion als Methode aufgerufen, so ist in this das Objekt verfügbar, das bei Methodenaufruf "links vom Punkt" stand. Auf diese Weise kann istGelöst() auf den aktuellen Zustand der _richtig Eigenschaft zugreifen.

Die Methoden implementieren eine offene Quizfrage.

prüfeAntwort
nimmt Antworten an. Solange nicht richtig gelöst wurde, wird anzVersuche erhöht und der Versuch mit der vorgegebenen Antwort verglichen. Das Vergleichsergebnis wird in der _richtig Eigenschaft gespeichert. Sobald _richtig auf true steht ist, werden keine Antworten mehr angenommen. Die Methode gibt auf jeden Fall den aktuellen Zustand von _richtig zurück.
punkte
ermittelt die Anzahl der Punkte, die man für die beantwortete Frage bekommt. Mit falscher Antwort oder mit zu vielen Versuchen gibt es 0 Punkte, ansonsten wird die interne _punkte Eigenschaft benutzt um die Punktzahl zu bestimmen.
istGelöst
dient zur Abfrage der internen _richtig Eigenschaft. Da sie intern ist, darf Code, der außerhalb des Objekts programmiert ist, diese Eigenschaft nicht benutzen.
Beispielhafte Aufrufreihenfolge
console.log(frage.punkte());            // 0, es hat noch keinen Versuch gegeben
console.log(frage.prüfeAntwort(42));    // -> guter Versuch, ergibt false
console.log(frage.punkte());            // -> 0 - ist noch nicht richtig gelöst
console.log(frage.prüfeAntwort(14));    // -> ergibt true
console.log(frage.istGelöst());         // -> ergibt true
console.log(frage.punkte());            // -> 10 - im zweiten Versuch gelöst

Für eine robuste Programmierung ist dieses Vorgehen allerdings unbefriedigend. Die Antworten sollten vor fremden Blicken geschützt sein, und die _richtig Eigenschaft sollte nicht von außen beschreibbar sein können. Dafür bietet sich der Einsatz von Closures und des Revealing Module Patterns an, mit dem ein Objekt wirklich private Daten halten kann.

ECMAScript 6: Abkürzende Schreibweisen und berechnete Methdodennamen

Mit ECMAScript 6 wurde eine vereinfachte Methodenschreibweise eingeführt, die mehr an vertraute Funktionsdefinitionen erinnert. Und auch für Methoden lassen sich berechnete Namen verwenden.

Beispiel
var methode = 'test',
    frage = {
       punkte() {
          return (!this._richtig || this.anzVersuche > this._punkte.length) ? 0 : this._punkte[this.anzVersuche-1];
       },
       [methode](istOk) { return !istOk; }
    };

Das frage-Objekt hat jetzt eine Methode punkte und eine Methode test(istOk). Berechnete Methodennamen sind nicht ganz so sinnlos, wie sie hier scheinen, denn in ECMAScript 6 gibt es gewisse Methoden (z. B. Iteratoren), deren Name ein ganz bestimmtes Symbol sein muss. Hier ist die Verwendung berechneter Methodennamen unverzichtbar, weil der Aufruf Symbol("iterator") jedesmal ein neues Symbol erzeugt und nicht das vordefinierte Symbol in Symbol.iterator.

Eigenschaften mit Getter und Setter

Es gibt Situationen, wo Objekteigenschaften zwar aus Nutzersicht wie eine Variable aussehen sollen, in der technischen Realisierung des Objekts aber nicht einfach als Variable dargestellt werden können. Ein Beispiel dafür sind die Objekte des DOM - hier gibt es Element-Eigenschaften wie innerHTML, wo beim Lesen der innere Elementbaum des DOM-Knotens traversiert werden muss, und beim Schreiben kommt noch die Anwendung aller CSS Klassen hinzu. Ein anderes Anwendungsbeispiel sind Webseiten, die dem Single-Page Muster folgen, hier erfolgt die Interaktion im Vordergrund zwischen HTML und JavaScript-Datenobjekten und erst im Hintergrund zwischen JavaScript und Server. Die Bindung zwischen HTML und JavaScript Objekten erfolgt über Ereignisregistrierungen; und wenn ich möchte, dass ein Schreibvorgang auf eine Objekteigenschaft ein Ereignis auslöst, dann geht auch das nur über eine Funktion, die dabei im Hintergrund aufgerufen wird.

Die für solche Aktivitäten erforderlichen Funktionen nennt man Getter und Setter. Eine Objekteigenschaft, die Getter und Setter verwendet, kann man in einem Objektliteral direkt erzeugen oder mit der Funktion Object.defineProperty() einem bestehenden Objekt hinzufügen. Die im Beispiel aufgerufenene changed-Funktion stellen Sie sich bitte als Teil eines Frameworks vor, das die oben erwähnte Objekt zu HTML Bindung realisiert.

Beispiel
1. Definition im Objektliteral
var person = {
   get name() {
     return this._name;
   }
   set name(neuerName) {
      var alterName = this._name;
      this._name = neuerName;
      this.changed('name', alterName, neuerName); 
   }
}
person.name = "Rolf";                        // Name zuweisen; der Setter löst das changed-Ereignis aus
console.log("Der Name ist " + person.name);  // Ruft den Getter auf
2. Definition mittels Object.defineProperty
var person = { };
Object.defineProperty(person, "name", {
   get: function() {
     return this._name;
   },
   set: function(neuerName) {
      var alterName = this._name;
      this._name = neuerName;
      this.changed('name', alterName, neuerName); 
   }
});

Property-Definitionen der zweiten Art sind besser in Konstruktorfunktionen oder Prototypen aufgehoben, die weiter unten vorgestellt werden.

Getter und Setter gibt es schon länger, aber nicht in allen Browsern und auch nicht überall gleich gelöst. Eine Standardisierung dazu hat sich erst mit ECMAScript 5 herausgebildet. Deshalb verwenden viele JavaScript Frameworks, die schon länger existieren, eine alternative Technik. Diese besteht darin, an Stelle eines einfachen Eigenschaftswertes eine Funktion zu definieren, die beim Aufruf prüft, ob sie einen Parameter erhalten hat. Wenn nicht, verhält sie sich wie ein getter, sonst wie ein Setter.

Kombinierter Getter und Setter
var person = {
   name: function(neuerName) {
      if (arguments.length === 0) {
         return this._name;
      } else {
         this.changed('name', neuerName); 
         this._name = neuerName;
         return this;
      }
   }
}
var derName = person.name();     // lesen
person.name("Hugo");             // setzen

Nachteil dieser Methode ist, dass man vergessen kann, es mit einer Funktion zu tun zu haben. Dann liest man Unsinn oder macht beim Schreiben das Property kaputt. Vorteil ist allerdings, dass man an Stelle eines einfachen Parameterwertes ein Funktionsobjekt hat, an das man wiederum Methoden anhängen kann. Beispielsweise realisiert KnockoutJS seine Observables (beobachtbare Eigenschaft) auf diese Weise und bietet über eine subscribe Methode die Möglichkeit, sich auf Änderungen am Wert der Eigenschaft direkt zu registrieren.

Objektliterale können mit Anweisungen verwechselt werden

Objektliterale sind aus Sicht von JavaScript ganz normale Werte, die als Teil eines Ausdrucks auftreten können. Bei JavaScript ist es allerdings an manchen Stellen nicht ganz eindeutig, ob ein Ausdruck beginnt oder eine Anweisung. Im Zweifel gibt JavaScript der Interpretation als Anweisung den Vorrang. Da die geschweiften Klammern des Objektliterals auch dazu da sind, einen Anweisungsblock zu erzeugen, würde JavaScript diese Zeile als Anweisung interpretieren und sich dann beklagen, dass er etwas findet womit es nicht rechnet.

Beispiel
    {
       marke: "BMW",
       hupen: function() { console.log("Dein "+this.marke+" macht Tüüüt"); }
    }.hupen();  // -> Unexpected Token

Es handelt sich hier um ein Objektliteral für ein Auto mit einer Methode hupen(), die sofort aufgerufen wird; ein spezieller Fall einer IIFE. Um sicher zu stellen, dass dieser Ausdruck auch als Ausdruck verarbeitet wird, muss er eingeklammert werden; entweder nur das Objektliteral oder alles.

Beispiel
   ({
      marke: "BMW",
      hupen: function() { console.log("Dein "+this.marke+" macht Tüüüt"); }
   }).hupen();   // So geht's
   ({
      marke: "BMW",
      hupen: function() { console.log("Dein "+this.marke+" macht Tüüüt"); }
   }.hupen());   // So auch

Konstruktoren und Prototypen

Eine Aufgabe, die sich beim Anlegen neuer Objekte immer wieder stellt, ist die Initialisierung. Dazu ist eine Funktion hilfreich, die beim Erzeugen einer bestimmten Art von Objekten automatisch aufgerufen wird. Eine solche Funktion nennt man Konstruktor.

Beachten Sie: Es ist eine Konvention, den Namen einer Konstruktorfunktion mit einem Großbuchstaben beginnen zu lassen, und alle anderen Funktionen mit einem Kleinbuchstaben.

Wenn man viele gleichartige Objekte erzeugen will, die alle eine gemeinsame Menge an Methoden anbieten sollen, ist es zwar möglich, aber ineffizient, diese Methoden in der Konstruktorfunktion immer einzeln an jedes neue Objekt anzuhängen. Um das zu vereinfachen, kennt JavaScript das Konzept der Vererbung mit Prototypen. Ein Prototyp ist Objekt, in dem einmalig die gewünschten Funktionen gespeichert werden. Danach erzeugt man beliebig viele weitere Objekte und legt fest, dass sie das Prototypobjekt verwenden sollen. Wenn man danach versucht, eine Eigenschaft aus einem Objekt zu lesen (was Methodenaufrufe mit einschließt), so schaut JavaScript zuerst in den Eigenschaften des Objektes selbst nach, und dann in den Eigenschaften des Prototypobjekts. Um Prototypen bereitstellen zu können, erzeugt JavaScript an jeder selbstdefinierten Funktion automatisch eine Eigenschaft prototype, die mit einem neuen, leeren Objekt vorbelegt ist. Diesem Objekt kann man nach Belieben Eigenschaften zuordnen.

Beispiel
var OffeneFrage = function(text, antwort, punkte) {
   this.text = text;
   this.antwort = antwort;
   this._punkte = punkte;
   this._richtig = false;
   this._anzVersuche = 0;
};
OffeneFrage.prototype.prüfeAntwort = function(versuch) { /* wie oben */ };
OffeneFrage.prototype.punkte = function() { /* wie oben */ };
OffeneFrage.prototype.istGelöst = function() { return this._richtig; };

// Erzeuge eine Offene Frage:
var frage = new OffeneFrage('Was bedeutet laudare', 'loben', [ 20, 10, 5],);

new - genauer betrachtet

Der new Operator ist dazu gedacht, Funktionen als Konstruktor zu nutzen. Er führt grob gesagt folgende Aktionen aus (wer es wirklich genau wissen will, möge in die Spezifikation von ECMAScript 6 schauen):

  • Erzeugen eines neuen, leeren Objekts
  • Speichern der prototype Eigenschaft der Konstruktorfunktion als Prototyp des neuen Objekts.
  • Ausführen der Konstruktorfunktion im Kontext des neuen Objekts (deshalb ist this im Konstruktor zum Ansprechen des neuen Objektes verwendbar). Innerhalb der Konstruktorfunktion sind beliebige Vorbereitungsarbeiten am Objekt möglich.
  • Das Ergebnis des new-Operators hängt davon ab, was die Konstruktorfunktion zurückgibt. Wenn sie einen Wert x mit typeof x === "object" zurückgibt, so wird x zum Ergebnis des new-Operators. Andernfalls ist das Ergebnis von new der Wert von this.
Beachten Sie: Die übliche Erwartungshaltung an new ist, dass ein neues Objekt erzeugt und zurückgegeben wird. Ein abweichendes Verhalten kann in Ausnahmefällen sinnvoll sein, z.B. wenn man bei new OffeneFrage(...) sicherstellen will, dass es jede Frage nur einmal im Quiz gibt und einen Cache aller erzeugten Fragen hält.
Beachten Sie auch: Es ist möglich, die prototype Eigenschaft der Konstruktorfunktion komplett mit einem eigenen Wert zu überschreiben. Der new Operator prüft allerdings, ob dieser Wert wirklich ein Objekt ist. Wenn nicht, wird die prototype Eigenschaft der Konstruktorfunktion ignoriert und statt dessen Object.prototype verwendet.
Wichtig: Verwechseln Sie nicht die prototype Eigenschaft einer Konstruktorfunktion mit ihrem eigenen Prototypen. Den Prototyp eines Objekts obj erreicht man seit ECMAScript 5 über die Methode Object.getPrototypeOf(obj). Vorher gab es schon die unstandardisierte Eigenschaft __proto__, die man abfragen und auch ändern konnte. Sie ist mit ECMAScript 6 in den Standard aufgenommen worden, aber nur für Webbrowser verpflichtend.
Beispiel
var Demo = function() {
   this.text = "Nur so";
};

console.log(Object.getPrototypeOf(Demo) === Demo.__proto__);    // true
console.log(Object.getPrototypeOf(Demo) === Demo.prototype);    // false !

Prototypketten

Prototypobjekte sind auch nur Objekte, und das bedeutet, dass sie ihrerseits ein Prototyp-Objekt haben können. JavaScript beginnt bei der Suche nach einer Eigenschaft des Objektes o mit dem Objekt selbst, setzt sie im Prototyp von o fort, dann im Prototyp des Prototypen, und so weiter. Zum Beispiel ist es so, dass die angeblich leere Eigenschaft OffeneFrage.prototype durch new Object() erzeugt worden ist und damit die Methoden von Object.prototype zur Verfügung hat. Das sind einige - schauen Sie einmal hier unter "vererbte Methoden" nach. Object.prototype ist dagegen gleich null.

Die Prototypkette kann von Ihnen noch weiter verlängert werden. Darauf geht der Artikel Vererbung näher ein. Und wenn Sie unbedingt möchten, können Sie auch eigene Objekte erzeugen, deren Prototyp null ist. Dazu verwenden Sie die Funktion Object.create().

Eigenschaften mit Getter und Setter in Konstruktor und Prototyp

Die oben gezeigte Möglichkeit, Eigenschaften mit Funktionen zum Ermitteln und Schreiben des Wertes auszustatten, lässt sich mit Konstruktorfunktionen oder Prototypobjekten an einer zentralen Stelle unterbringen. Das folgende Beispiel zeigt eine Konstruktorfunktion, die die Eigenschaften istGelöst und punkte als Eigenschaft mit Getter bereitstellt. Dadurch, dass der Setter fehlt, können diesen Eigenschaften keine Werte zugewiesen werden.

Beispiel
var OffeneFrage = function(frage, antwort, punkte) {
   var richtig = false,
       versuche = 0,
       punkteTabelle = punkte.slice();          // Kopie machen!

   Object.defineProperty(this, "istGelöst", {
      get: function() {
         return richtig;
      }
   });
   Object.defineProperty(this, "punkte", {
      get: function() {
         if (!richtig || versuche > punkteTabelle.length)
             return 0;
         else
             return punkteTabelle[versuche-1];
      }
   });
   this.text = frage;
   this.antwort = antwort;
};
Beachten Sie: Die Getter-Funktionen greifen auf die lokalen Variablen richtig, versuche und punkteTabelle der Konstruktorfunktion zu. Dass das funktioniert, ist eigentlich erstaunlich, weil der Aufruf dieser Funktionen stattfindet, nachdem der Konstruktor schon beendet ist, und lokale Variablen beenden ihre Existenz typischerweise mit dem Gültigkeitsbereich, im dem sie definiert wurden. Hier kommt das Konzept der Closure ins Spiel, das es Funktionen ermöglicht, Variablen aus dem Kontext ihrer Definition einzukapseln und immer noch zu verwenden, nachdem der Kontext eigentlich schon gar nicht mehr da ist. Näheres dazu finden Sie im verlinkten Wiki-Artikel.

Allerdings hat dieses Vorgehen einen Nachteil: Die Methoden der Frage müssen jetzt wieder in den Konstruktor wandern, weil sie sonst keinen Zugriff auf die Variablen richtig, versuche und punkteTabelle haben. Damit haben wir jetzt echte private Objekteigenschaften, aber bezahlen damit durch einen Laufzeitnachteil bei der Objektanlage, weil jedes neu erzeugte Frage-Objekt neu seine Methoden zugewiesen bekommen muss.

Eine zur Laufzeit schnellere Lösung verwendet das Revealing Module Pattern und die in ECMAScript 6 neu eingeführten Klasse Symbol. Mit Symbol lässt sich ein Name für eine Eigenschaft erzeugen, durch den die Eigenschaft nur mit Kenntnis des Symbol erreichbar ist. Wenn das Symbol in einer Closure steckt, ist das fast nicht mehr möglich. Um das Ganze für Einsteiger etwas weniger kryptisch aussehen zu lassen, wurde die Erzeugerfunktion für die OffeneFrage Klasse als eigene, benannte Funktion angelegt.

Beispiel
function erzeugeOffeneFrage() {
   var symAntwort = Symbol("antwort"),
       symGelöst = Symbol("gelöst"),
       symVersuche = Symbol("versuche"),
       symPunkteTab = Symbol("punkte"),
       COffeneFrage = function(frage, antwort, punkte) {
          this.frage = frage;
          this[symAntwort] = antwort;
          this[symPunkteTab] = punkte;
          this[symRichtig] = false;
          this[symVersuche] = 0;
       };

   COffeneFrage.prototype = { 
      get anzVersuche()  { return this[symVersuche]; },
      get istGelöst()    { return this[symRichtig];  },
      get punkte()       {
         if (!this.istGelöst || this.anzVersuche > this[punkteTab].length)
            return 0;
         else
            return this[symPunkteTab][this.anzVersuche-1];
      },
      prüfeAntwort: function() (versuch) {
         if (!this.istGelöst)
         {
            this[symVersuche]++;
            this[symRichtig] = (versuch == this[symAntwort]);
         }
         return this.istGelöst;
      };
   };
   Object.freeze(COffeneFrage.prototype);
   return COffeneFrage;
};

OffeneFrage = erzeugeOffeneFrage();

var frage = new OffeneFrage("7 * 8", "56", [10, 5]);
frage.prüfeAntwort("54");
if (frage.istGelöst)
   console.log("Antwort war falsch");
)();

Die Aufgabe der erzeugeOffeneFrage Funktion ist es vor allem, einen geschlossenen Kontext für die privaten Elemente der OffeneFrage-Klasse herzustellen. Sie wird einmal ausgeführt, gibt die Konstruktorfunktion für OffeneFrage zurück und wird danach nicht mehr gebraucht. Den erzeugten Konstruktor speichert man global oder in einem dafür eingerichteten Namespace-Objekt.

Es gibt fünf private Elemente in der Erzeugerfunktion. Zunächst view Symbole als Schlüssel für die privaten Eigenschaften, die später in den erzeugten Frage-Objekte gespeichert werden sollen, und dann die Funktionen COffeneFrage. Das ist die Konstruktorfunktion, die von der Erzeugerfunktion zurückgegeben wird. Sie erzeugt die für das Objekt benötigten Daten-Eigenschaften. Die Frage ist öffentlich, der Rest privat.

Die eigentlich Funktionalität der Klasse wird im Prototypen abgebildet. Die zuvor als Methoden bereitgestellten Eigenschaften anzVersuche, istGelöst und punkte sind jetzt Eigenschaften mit Getter-Funktion und können deshalb wie eine Dateneigenschaft verwendet werden. Schreibversuche auf diese Eigenschaften laufen aber ins Leere.

Der Aufruf der ECMAScript 5 Funktion Object.freeze() für das Prototyp-Objekt stellt sicher, dass der Prototyp nicht mehr manipulierbar ist. Das ist natürlich nicht zwingend erforderlich, es soll nur die Möglichkeit aufzeigen.

In prüfeAntwort() zeigt sich eine Besonderheit von this. An der Stelle, wo die Methoden als Funktionsobjekt erzeugt werden, bezeichnet this eigentlich das globale window-Objekt, weil die umgebende Erzeugerfunktion nicht als Methode aufgerufen wird. Aber this ist keine normale Variable und wird nicht in eine Closure mitgenommen, es erhält seinen Wert erst unmittelbar vor dem Aufruf der Funktion, in der es genutzt wird. Wenn man also schreibt:

  frage.prüfeAntwort("42")

so geschieht folgendes:

  • Suche einer "prüfeAntwort" Eigenschaft im frage-Objekt - schlägt fehl
  • Suche einer "prüfeAntwort" Eigenschaft im OffeneFrage.prototype-Objekt - gelingt und liefert ein Funktionsobjekt
  • Dieses Funktionsobjekt wird aufgerufen. Dabei erhält this das frage-Objekt und "42" wird als Argument übergeben.
  • Das Ergebnis dieses Funktionsaufrufs ist das Ergebnis des Methodenaufrufs.

Die gezeigte Lösung ist nicht GANZ wasserdicht, weil man mit Object.getOwnPropertySymbols() die in einem Objekt verwendeten Symbole ermitteln und dann per toString() nach dem Symbol-Namen suchen kann. Für eine Praxislösung würde man daher Symbole ohne Namen verwenden, bzw. ein Werkzeug verwenden, das bei Überstellung des JavaScript-Codes auf den Webserver alle Symbolnamen entfernt. Das macht es dem unbefugten Benutzer zumindest schwerer. Vollständige Privatheit lässt sich aber durch Einsatz von Symbolen nicht erzielen.

Objekte als Datenstrukturen

JavaScript Objekte sind auf Grund ihrer flexiblen Erweiterbarkeit auch als allgemeiner Speicher von Name-Wert Paaren nutzbar, vergleichbar mit dem Array aus PHP, Map aus Java oder Dictionary aus C#. Der Begriff "assoziatives Array", der für solche Konstrukte in PHP gerne fällt, ist in JavaScript allerdings irreführend. JavaScript-Arrays sind - was den Array-Anteil dieser Objekte betrifft - nicht assoziativ. Das folgende Beispiel zeigt eine zweistufige Objekthierarchie: Ein Dictionary (fuhrpark), das Autos speichert und das Autokennzeichen als Schlüssel verwendet.

Beispiel
var fuhrpark = {};
fuhrpark['K-TX 4711'] = new Auto('Mercedes', 2011, 'taxigelb');
fuhrpark['B-TA 123'] = new Auto('Audi', 2007, 'neongelb');
fuhrpark['M-AX 815'] = new Auto('BMW', 2013, 'taxigelb');

// fuhrpark ist KEIN Array:
console.log("Der Fuhrpark enthält " + fuhrpark.length + " Fahrzeuge");
// Ausgabe: "Der Fuhrpark enthält undefined Fahrzeuge"

var suche = 'K-TX 4711';
console.log("Das Auto mit Kennzeichen "+suche+" ist ein " + fuhrpark[suche].marke);

delete fuhrpark['M-AX 815'];     // Münchener Taxi aus der Liste entfernen

Komplexere Objektkonstruktionen

Objekte sind frei zuweisbare Werte, daher kann der Wert einer Objekteigenschaft wiederum ein Objekt sein. Die Tiefe, in der das geschieht, ist nicht begrenzt. Die Komplexität der Ausdrücke, die man aufschreiben muss um den so entstehenden Objektketten zu folgen, dann allerdings auch nicht.

Beispiel
// Einen Opel in den Fuhrpark
fuhrpark['GG-AO 123'] = new Auto('Opel', 2013, 'schwarz');

// Diesem Fuhrpark-Opel eine neue Eigenschaft <code>besitzer</code> zuordnen und ein Objekt darin speichern
fuhrpark['GG-AO 123'].besitzer = { name: 'Adam', wohnort: 'Rüsselsheim' };

// Erster Buchstabe des Wohnorts
console.log(fuhrpark['GG-AO 123'].besitzer.wohnort.substr(0,1));

Wenn Sie den Zugriff bei "Erster Buchstabe des Wohnorts" nicht gleich verstehen - gehen Sie strikt von links nach rechts vor. Zuerst wird die Eigenschaft 'GG-AO 123' des Objekts fuhrpark gelesen und liefert ein Auto-Objekt. Aus diesem Auto-Objekt wird der Besitzer ausgelesen, vom Besitzer der Wohnort, und auf die so erhaltene Zeichenkette wird die Methode substr() von String.prototype angewendet.

Beachten Sie, dass der Beispielcode so nicht für den realen Einsatz geeignet ist. In der Praxis wird man prüfen müssen, ob die angesprochenen Objekte wirklich existieren, weil man andernfalls mit Laufzeitfehlern und Scriptabbrüchen rechnen muss. Entweder setzt man auf try...catch, oder muss jeden Zugriff einzeln auf Erfolg prüfen.
Beispiel
Der Code zur Prüfung auf Vorhandensein der referenzierten Objekte wird hier in eine Funktion ausgelagert. Man könnte auch eine Methode von Auto daraus machen
function getWohnortInitiale(auto) {
   if (auto && auto.besitzer && auto.besitzer.wohnort)
      return auto.besitzer.wohnort.substr(0,1);
   else
      return '';
}

console.log("Wohnortinitialie: " + getWohnortInitiale(fuhrpark['GG-AO 123']

Auflisten von Objektinhalten

Nicht immer sind die Namen der Eigenschaften bekannt, die ein Objekt hat. Der oben gezeigte Fuhrpark ist ein gutes Beispiel dafür. Sie wollen bestimmt nicht in einem separaten Array nachhalten, welche Autokennzeichen darin gespeichert sind und welche nicht. Das würde die Idee, das Kennzeichen als Schlüssel zu verwenden, ad absurdum führen.

JavaScript bietet mehrere Möglichkeiten, mit denen man die vorhandenen Eigenschaften eines Objekts durchlaufen kann. Allen ist gemein, dass die Reihenfolge, in der die Eigenschaften eines Objekts durchlaufen werden, undefiniert ist. Wenn eine bestimmte Reihenfolge benötigt wird, muss man durch eigenes Sortieren dafür sorgen. Es ist aber immerhin so, dass die Reihenfolge, in der die Eigenschaften eines bestimmten Objekts durchlaufen werden, bei allen drei Varianten die gleiche ist.

Für die Eigenschaften eines Objektes ist einstellbar, ob sie aufzählbar sein sollen oder nicht. Wie man das macht, soll später erläutert werden, hier ist für das Verständnis nur wichtig, dass es diese Möglichkeit gibt. Beispielsweise sind sämtliche Methoden in Object.prototype nicht aufzählbar. Selbst angelegte Eigenschaften sind standardmäßig immer aufzählbar.

for (prop in obj)

Die For...in-Schleife existiert seit Anbeginn von JavaScript und durchläuft ihren Schleifenrumpf einmal für jede aufzählbare Eigenschaft eines Objekts. In der Laufvariablen prop steht der Name des Eigenschaft zur Verfügung, den Wert kann sich der Schleifenrumpf mittels obj[prop] beschaffen. for...in verarbeitet nicht nur die Eigenschaften, die am Objekt selbst definiert sind, sondern auch alle, die entlang der Prototypenkette zur Verfügung stehen. Wenn man die vom Prototypen geerbten Eigenschaften nicht verarbeiten will, muss man die Methode hasOwnProperty(name) Methode verwenden, um auf die eigenen Eigenschaften zu filtern, oder Object.keys() verwenden.

Beispiel
var fuhrpark = { },          // aus obigem Beispiel
    auto;

for (kennz in fuhrpark) {
   auto = fuhrpark[kennz];
   console.log("Das Fahrzeug mit Kennzeichen " + kennz +
               " ist ein " + auto.marke + " von " + auto.baujahr);
}


Beachten Sie, dass es zu mehr oder weniger zufälligen Ergebnissen führt, wenn man im Rumpf einer for...in Schleife einem Objekt Eigenschaften hinzufügt oder wegnimmt. JavaScript produziert keinen Laufzeitfehler, aber ob eine im Schleifenrumpf erzeugte neue Eigenschaft im weiteren Verlauf der Schleife durchlaufen wird, ist nicht vorhersehbar.

Object.keys

Die keys() Methode des vordefinierten JavaScript-Objektes Object ist eine Erweiterung von ECMAScript 5.1 und kann als der Kern der for...in Schleife angesehen werden. Sie liefert alle aufzählbaren Eigenschaften eines Objektes als ein Array von Zeichenketten, ohne dabei die Prototypen hinzuzunehmen.

Beispiel
var fuhrpark = { },          // aus obigem Beispiel
    kennzeichen = Object.keys(fuhrpark),
    i,
    kennz, auto;

for (i=0; i<kennzeichen.length; i++) {
   kennz = kennzeichen[i];
   auto = fuhrpark[kennz];
   console.log("Das Fahrzeug mit Kennzeichen " + kennz +
               " ist ein " + auto.marke + " von " + auto.baujahr);
}

Object.getOwnPropertyNames

Die Methode Object.getOwnPropertyNames() stammt ebenfalls aus ECMAScript 5.1 und unterscheidet sich von Object.keys() nur dadurch, dass sie auch die Eigenschaften auflistet, die als nicht-aufzählbar markiert wurden. Möchte man beim Verarbeiten dieser Auflistung wissen, ob ein bestimmtes Property aufzählbar war oder nicht, so muss man das für jedes Property durch Object.getOwnPropertyDescriptor() ermitteln.

Attribute von Objekteigenschaften

Eine Objekteigenschaft prop verfügt selbst im einfachsten Fall nicht nur über einen Namen und einen Wert. Das Verhalten einer Eigenschaft wird über den sogenannten Eigenschaftdeskriptor festgelegt, bei dem es sich um ein Objekt mit vier Eigenschaften handelt. Der Wert von prop kann eine davon sein. Die Eigenschaften des Deskriptors bilden die Attribute der Objekteigenschaft.

Es gibt zwei Arten von Eigenschaftdeskriptoren: Deskriptoren für Daten-Eigenschaften (data descriptor) und Deskriptoren für Eigenschaften mit Zugriffsfunktionen (accessor descriptor). Beide Arten haben zwei Attribute gemeinsam:

enumerable
Ist die Eigenschaft für die Methode Object.keys sichbar oder nicht. Der Defaultwert ist false.
configurable
Können die Attribute dieser Eigenschaft durch Object.defineProperty geändert werden. Der Defaultwert ist false.

Bei Daten-Eigenschaften kommen diese zwei Attribute hinzu:

writable
Sind schreibende Zugriffe auf diesen Wert erlaubt. Der Defaultwert ist false und erzeugt eine "read-only" Eigenschaft.
value
Der Wert dieser Eigenschaft. Default ist undefined
Beachten Sie: Das Attribut für den Schreibschutz heißt writable, nicht writeable. Object.defineProperty() akzeptiert möglicherweise Deskriptoren, die writeable verwenden. Davon ist im ECMAScript-Standard jedoch nicht die Rede und Object.getOwnPropertyDescriptor() liefert immer Deskriptoren mit writable als Eigenschaftsname.

Eigenschaften mit Zugriffsfunktionen besitzen statt dessen diese Attribute:

get
Die getter-Funktion. Der Defaultwert ist undefined und führt zu einer "write-only" Eigenschaft.
set
Die setter-Funktion. Der Defaultwert ist undefined und führt zu einer "read-only" Eigenschaft.

Die genannten Defaultwerte sind dann von Bedeutung, wenn ein Eigenschaftdeskriptor an Object.defineProperty() übergeben wird. Anders ist es, wenn eine Eigenschaft durch ein Objektliteral oder durch Zuweisung eines Wertes an einen neuen Eigenschaftsnamen entsteht. Solche Eigenschaften sind immer enumerable und configurable, und so erzeugte Dateneigenschaften sind writable.

Es wurde bereits angedeutet, dass eine Eian Stelle eines Wertes der Wert 

Bei den bisher gezeigten Objekteigenschaften handelte es sich um sogenannte Dateneigenschaften (data properties). Solche Eigenschaften speichern den Wert direkt.

Im Gegensatz dazu stehen Eigenschaften mit Zugriffsfunktionen (accessor properties). Hier legt der Programmierer zwei Funktionen fest, den getter und den setter. Lesen einer Eigenschaft führt zum Aufruf der getter-Funktion, Schreiben der Eigenschaft führt zum Aufruf der setter-Funktion. Programmiert man beide Funktionen, erhält man eine lesbare und schreibbare Eigenschaft. Programmiert man nur einen getter, ergibt das eine schreibgeschützte Eigenschaft. Programmiert man nur einen setter, erhält man eine gegen Lesen geschützte Eigenschaft.

Attribute einer Objekteigenschaft lassen sich setzen, wenn man die Funktionen Object.defineProperty oder Object.defineProperties verwendet. Diese Funktionen können neue Eigenschaften erzeugen oder bestehende - in Grenzen - umkonfigurieren. Welche Attribute es gibt und wie man sie verwendet ist in diesen Artikeln beschrieben.

Objekteigenschaften mit Zugriffsfunktionen lassen sich darüber hinaus in Objektliteralen festlegen. Bitte lesen Sie dazu die Artikel Getter und Setter.

Definition im Objektliteral

Diese Möglichkeit gibt es schon lange - außer im Internet Explorer.

Beispiel
var obj = {
   name: 'Audi',
   get farbe() { return this.___farbe || 'rot'; },
   set farbe(neueFarbe) { this.___farbe = neueFarbe; },
   hupen: function() { console.log("Ich bin " + this.farbe() + " und mache Möööp!"); }
};
auto.hupen();     // -> Ich bin rot und mache Möööp!

... immer noch nicht ganz fertig

Quellen