JavaScript/Tutorials/OOP/Getter und Setter

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

ToDo (weitere ToDos)

Dies ist nur ein (zusammenkopierter) Stub.

Ich lasse ihn stehen, um Felix' geplanter Überarbeitung des JS Bereichs nicht querzuschießen. Rolf b (Diskussion) 22:05, 1. Mär. 2024 (CET)

  • Wo soll dieser Abschnitt in unserer OOP-Reihe eingeordnet werden?
  • Gibt es einen anderen möglichen Platz?



--Matthias Scharwies (Diskussion) 21:54, 23. Apr. 2023 (CEST)

Mit get können Sie die Eigenschaft eines Objekts an eine Funktion binden, die aufgerufen wird, wenn die Eigenschaft angeschaut wird.

Beispiel
{get eigenschaft() { ... } }
{get [ausdruck]() { ... } }


Folgende Angaben sind möglich:

  • eigenschaft: Eigenschaft, die mit der angegebenen Funktion verbunden werden soll
  • ausdruck:

Siehe auch


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.

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.



Weblinks