JavaScript/Tutorials/OOP/Objekte und ihre Eigenschaften

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Text-Info

Lesedauer
30min
Schwierigkeitsgrad
mittel
Vorausgesetztes Wissen
Kenntnisse in
Einstieg in JavaScript
Einstieg in das DOM


Objekte in JavaScript sind fest umgrenzte Datenelemente mit Eigenschaften und oft auch mit objektgebundenen Methoden (Funktionen).

Objekte in JavaScript können mit Gegenständen im wirklichen Leben verglichen werden. So wie ein Mensch anhand seiner Eigenschaften wie Name, Alter, Wohnort oder Hobbys beschrieben wird, können JavaScript-Objekte Eigenschaften (properties) haben, die die Merkmale beschreiben.

Objekte[Bearbeiten]

Erzeugen eines Objekts
const personA = new Object();
So - personA ist ein Objekt! Einem Objekt können wir Eigenschaften zuweisen:
personA.name = "Ann";
personA.age  = 31;
Für das mit const[1] neu erzeugte Objekt werden nun zwei Eigenschaften vergeben, einmal die Eigenschaft name und einmal age.

Auf die Eigenschaften eines Objekts können Sie am einfachsten mit der Punkt-Notation (dot-syntax) zugreifen. Dabei können Sie Objekten jederzeit neue Eigenschaften hinzufügen, oder vorhandene Eigenschaften verändern, indem mit dem Punkt-Operator der gewünschte Eigenschaftsnamen notiert und mit dem Istgleichzeichen der gewünschte Wert zugewiesen wird.

Wie bei allen Variablen gelten dabei auch für Objektnamen und Eigenschaftsnamen die Regeln für selbstvergebene Namen.

neue Objekte erzeugen[Bearbeiten]

Es gibt mehrere Wege ein Objekt zu erzeugen:

  • mit dem Schlüsselwort new in einer Anweisung new Object();
  • durch ein Literal (engl. für wörtlich)
  • mit der Methode Object.create()


Erzeugung mit Schlüsselwort "new"
const personA = new Object();
personA.name   = 'Ann';
personA.age    = 31;
personA.adult  = true;
Erzeugung in Literalschreibweise
const personB = {
    name:  "Bob",
    age:   29,
    hobby: "drinking"
};

Das war einfach. Jetzt haben Sie also zwei Objekte: personA und personB. Sie haben jeweils die beiden Eigenschaften name und age. personB hat eine zusätzliche Eigenschaft namens hobby. Lesen Sie das Beispiel noch einmal, wenn Sie sich darüber nicht im Klaren sind, denn schon der nächste Satz wird ein sehr wichtiger Satz sein.

Irgendwo gibt es auch ein drittes Objekt, das zwei Eigenschaften hat - personA und personB. Dies gibt uns einen ersten Einblick in OO-Programmierung. Alles befindet sich innerhalb eines Objekts.


Hinweis

Die Literalschreibweise ist immer dann nützlich, wenn es sich um ein Objekt handelt, das nur einmal benötigt wird. Auch wenn man keine Methoden im Objekt benötigt, kann man so effizient und vor allem gut lesbar ein Objekt erzeugen.
Die Schreibweise eines Objektliterals war übrigens die Grundlage für das mittlerweile im Internet sehr verbreitete JSON-Format.

Eigenschaften untersuchen[Bearbeiten]

Um ein Objekt und seine Eigenschaften zu untersuchen, können Sie das erste Beispiel in einem neuen Tab öffnen und im Seiteninspektor (F12 ) untersuchen.

Untersuchung mit Console API ansehen …
const personB = {
    name:  "Bob",
    age:   29,
    hobby: "drinking"
};
    
console.log(personB.name);
console.log(personB.hasOwnProperty("age"));
console.log(personB);

Dieses erste Live-Beispiel wiederholt den oben vorgestellten Code:

  • Es gibt ein Objekt personB in Literalschreibweise
  • Mit console.log wird das Objekt und seine name-Eigenschaft (mit Punktoperator verbunden) aufgerufen und der Wert ausgegeben.
  • Mit HasOwnProperty("age") wird überprüft, ob eine solche Eigenschaft existiert.
  • Mit console.log(personB) wird das Objekt mit allen Eigenschaften und dazugehörigen Werten ausgegeben.

Wenn Sie nun auf das graue Dreieck vor Object klicken, öffnet sich eine weitere Ansicht mit allen Schlüssel-Wert-Paaren und einem weiteren Dreieck mit dem Text <prototype>: Object { … } (in Chrome zwei eckige anstelle der spitzen Klammern).

OOP-1.png

OOP-2.png

Unter dem Object-Konstruktor sehen Sie eine Reihe von Eigenschaften. Alle diese Eigenschaften stammen aus dem globalen Prototyp von Object. Wenn Sie genau hinsehen, werden Sie auch unsere versteckte hasOwnProperty bemerken.[2]

Alle Objekte haben Zugriff auf den Prototyp des Objekts. Sie besitzen diese Eigenschaften nicht, haben aber Zugriff auf die Eigenschaften des Prototyps.

Es gibt mehrere Möglichkeiten auf Eigenschaften von Objekten zugreifen:

  • for...in: Schleife, durchläuft alle aufzählbaren Eigenschaften eines Objekts und seiner Prototypenkette
  • Object.keys(): gibt einen Array mit allen aufzählbaren Eigenschaftsnamen aus
  • Object.getOwnPropertyNames(): gibt einen Array mit allen Eigenschaftsnamen aus

Methoden[Bearbeiten]

Objekte können nicht nur irgendwelche Daten als Eigenschaften haben, sondern auch Funktionalitäten speichern. Diese Funktionalitäten werden im Grunde wie reguläre Funktionen definiert, sind als Eigenschaft eines Objektes gespeichert, was zusätzliche Möglichkeiten bringt. Man spricht in diesem Fall von Methoden.

Hinweis

Methoden sind auch nur Eigenschaften eines Objektes, aber ihr Wert ist eben ein Funktionsobjekt.


Methode sayName() ansehen …
personA.sayName = function () {
      return `My name is ${personA.name} and I am ${personA.age}!`;
    }
    
const personB = {
    name:  "Bob",
    age:   29,
    hobby: "drinking",
    sayName () {
      return `My name is ${personB.name} and I am ${personB.age}!`;
    }
}
    
console.log(personA.sayName());
console.log(personB.sayName());

Beide Objekte enthalten nun eine Eigenschaft sayName, der aber kein Wert, sondern eine Funktion zugewiesen ist. Diese Funktion lässt sich wie jede andere auch aufrufen.

Der Rückgabewert der Funktion im Beispiel ist ein Template-Literal, dessen String innerhalb der Backticks `…` noch in ${…} eingeschlossene JavaScript-Ausdrücke, nämlich die Eigenschaften name und age enthält.




Beide Funktionen beziehen sich auf Eigenschaften ihres Objekts. Könnte man dies nicht klarstellen, ohne jeweils den Namen des Objekts nennen zu müssen? So wie in der Sprache ein Pronomen auf ein Objekt oder eine Person zeigt?

this[Bearbeiten]

Das Schlüsselwort this verweist in einer Funktion oder Methode auf den Kontext, in dem sie aufgerufen wurde. Bei einem Methodenaufruf ist dieser Kontext das Objekt, auf dem die Methode aufgerufen wurde.

  • JavaScript 1.1
  • Chrome
  • Firefox
  • IE
  • Opera
  • Safari
Rückbezug mit this ansehen …
personA = {};
personA.sayName = function () {
      return (`My name is ${this.name} and I am ${this.age}!`);
    }
    
const personB = {
    name:  "Bob",
    age:   29,
    hobby: "drinking",
    sayName () {
      return (`My name is ${this.name} and I am ${this.age}!`);
    }
}
    
console.log(personA.sayName());
console.log(personB.sayName());

Die beiden Methoden enthalten nun anstelle des Objektnamens mit dem Schlüsselwort this einen Bezug auf das aktuelle Objekt. Durch die Verwendung von this wird der Code übersichtlicher, da klar ist, dass sich die entsprechenden Eigenschaften auf das aktuelle Objekt beziehen.

Vorsicht: this ohne Methodenkontext[Bearbeiten]

Beachten Sie: Der Aufrufkontext, also die Bindung einer Methode an das Objekt, zu dem sie gehört, wird im Moment des Aufrufs hergestellt. Wenn JavaScript einen Ausdruck wie personA.sayName() vorfindet, dann - 'und nur dann! - wird für diesen Aufruf der Wert von this auf personA gesetzt.

Prinzipiell finden in diesem Ausdruck aber zwei Dinge statt! Erstens: Auslesen des Funktionsobjekts aus der Eigenschaft sayName und zweitens: Aufrufen der Funktion. Passiert das nicht gemeinsam, geht der Kontext verloren, wie das folgende Beispiel zeigt.

Methode ohne Kontext
// Wir verwenden das personB Objekt aus dem vorigen Beispiel!
let sayer = personB.sayName;
let essay = sayer();
console.log(essay);      // ???

Auf diese Weise wird personB.sayName nicht als Methode, sondern wie eine normale Funktion aufgerufen. Nun hängt der Wert von this davon ab, ob sayName im strict mode ausgeführt wird. Ohne den strict mode finden Sie in this das globale Objekt (window-Objekt), das die Eigenschaft name besitzt, aber nicht age. Die Ausgabe wäre damit "My name is and i am undefined". Das kann schwer zu findende Programmfehler hervorrufen, falls man bei einem Methodenaufruf das Kontextobjekt vergisst, und deshalb bekommt eine Funktion im strict mode standardmäßig undefined als this-Objekt übergeben. Die Folge ist, dass der Aufruf zu einem TypeError führt.

Nun ist es in JavaScript üblich, Funktionen als Parameter übergeben zu können. Wie übergibt man dann Methoden als Parameter, wenn dabei der Kontext verloren geht?

Die stumpfsinnige Methode wäre, den Methodennamen als Zeichenkette zu übergeben und die Index-Schreibweise zu verwenden: object[methodenname](...). Damit sind der Abruf des Funktionsobjekts und der Funktionsaufruf wieder beisammen, und object wird zum Kontext des Aufrufs. Der Nachteil ist, dass Sie zwei Argumente übergeben müssen: Das Objekt und den Methodennamen. Vor allem muss der Empfänger dieser Argumente zwischen einem Funktionsaufruf und einem Methodenaufruf unterscheiden können. Das ist unschön.

Eine bessere Möglichkeit ist es, einen Adapter zu erzeugen, der die Methode mit einem festgelegten Kontext aufruft. Den können Sie als kleine Funktion selbst schreiben, oder Sie verwenden die Methode bind, die jede Funktion anbietet. Bitte folgen Sie dem Link für ein Beispiel.

Die dritte Möglichkeit ist, den Kontext im Moment des Aufrufs herzustellen. Dafür dienen die Methoden call und apply, die ebenfalls jede Funktion anbietet. Diese Variante ist dann nützlich, wenn Sie beabsichtigen, eine Funktion für unterschiedliche Objekte aufzurufen.




Wenn wir nun eine weitere Person anlegen wollen, müssen wir alles kopieren und das Objekt und die Werte ändern. Geht das auch einfacher?

Konstruktoren[Bearbeiten]

Wenn man mehrere Objekte von einer Art benötigt, lohnt sich der Einsatz eines sogenannten Konstruktors. Ein Konstruktor ist im Wesentlichen eine Funktion, die ein Objekt baut. Konstruktoren werden mit dem Schlüsselwort new verwendet.

Konstruktor-Funktion zur Erzeugung beliebig vieler Personen ansehen …
function Person (firstName, lastName, age) {
  this.firstName = firstName
  this.lastName = lastName
  this.age = age

  this.sayName = function () {
    return (`My name is ${this.firstName} ${this.lastName} and I am ${this.age}!`)
  }
}

// Creating a new person with the `new` keyword
const ann = new Person('Ann', 'Mustermann', 31);
const bob = new Person('Bob', 'Mustermann', 29);
const cem = new Person('Cem', 'Mustermann', 2);
    
console.log(ann.sayName());
console.log(cem.sayName());

Die Funktion Person nimmt drei Parameter entgegen, deren Werte den Eigenschaften firstName, lastName und age zugewiesen werden.

Mit Hilfe des Schlüsselwortes new dient diese Funktion Person als Konstruktorfunktion, mit deren Hilfe nun drei Objekte erzeugt werden.

Information

Ein Konstruktor ist eine Funktion, die beim Erzeugen eines neuen Objekts aufgerufen wird. Seine Aufgabe besteht darin, das neue Objekt in einen erwünschten Anfangszustand zu versetzen. Die dadurch entstandenen Objekte werden Instanzen (Exemplare, Abkömmlinge) dieses Konstruktors genannt. Als Analogie zur objektorientierten Programmierung mit Klassen wird der Name der Konstruktorfunktion auch als Klassenname aufgefasst.


Beachten Sie: Die eigentliche Objekterzeugung wird von JavaScript übernommen und findet statt, bevor die Konstruktorfunktion ausgeführt wird. Das bedeutet: wenn Sie eine Konstruktorfunktion ohne new aufrufen, wird diese Funktion ausgeführt, aber kein Objekt erzeugt. Der Wert von this wird dann durch die Regeln für this beim Aufruf normaler Funktionen bestimmt, das heißt: es enthält entweder undefined oder das globale Objekt.
Details finden Sie im Artikel zum new Operator. Mit new.target können Sie unterscheiden, ob der Funktionsaufruf mit oder ohne new erfolgte.



Der Konstruktor weist an this.sayName eine Funktion zu. Funktionen sind Objekte, und jede Funktion trägt den Kontext, in dem sie definiert wurde, mit sich (Closure). Das ist aufwändig und kostet Zeit, vor allem, wenn ein Objekt viele Methoden bekommen soll. Geht das nicht besser?

Prototypen[Bearbeiten]

Ein Objekt besitzt zum einen die Eigenschaften und Methoden, die direkt in ihm gespeichert wurden. Darüber hinaus kennt jedes Objekt in JavaScript einen sogenannten Prototypen, das ist ein Objekt, in dem JavaScript nachschaut, wenn man aus einem Objekt eine Eigenschaft auslesen will, die dort nicht gespeichert wurde.

Prototypen - Der Erste Kontakt
const person = {
   name: 'Mustermann',
   vorname: 'Erika'
}

console.log(person.vorname);                   // Erika
console.log(person.geburtsDatum);              // undefined
console.log(person.hasOwnProperty("vorname")); // true - aber....??

Die Ausgabe undefined für person.geburtsDatum ist nachvollziehbar, diese Eigenschaft wurde nicht erzeugt. Aber was ist mit dem true, das der Aufruf der Methode hasOwnProperty liefert? Das Ergebnis true klingt für eine Methode mit diesem Namen plausibel, denn das Objekt in person besitzt eine Eigenschaft "vorname". Aber wo, bitte schön, kommt die Methode her?

Hier kommt der Prototyp ins Spiel. Objekte, die Sie als Objektliteral notieren, bekommen automatisch das Objekt als Prototyp, das JavaScript in der Eigenschaft prototype des Systemobjekts Object bereitstellt. Und dort finden Sie die hasOwnProperty-Methode. Die Ausführungsumgebung von JavaScript stellt also fest, dass das Objekt in der Variablen person keine Eigenschaft namens hasOwnProperty besitzt, und schaut als nächstes im Prototypobjekt nach. Dort gibt es sie, und die Methode wird von dorther geholt. Wichtig ist nun, wie in diesem Fall this gebunden wird. Die Methode ist zwar in Object.prototype gespeichert, aber der Aufruf findet für das Object in person statt. Deshalb ist person der Aufrufkontext, den die hasOwnProperty Methode in this vorfindet und wofür sie ihre Aufgabe durchführt.

Information

Den Zugriff auf das Prototyp-Objekt, den JavaScript für unbekannte Eigenschaften und Methoden durchführt, nennt man Vererbung, genauer gesagt: prototypische Vererbung. Das Thema wird im nachfolgenden Artikel weiter vertieft.


Beachten Sie: JavaScript greift nur beim Lesen auf den Prototypen zurück. Wenn Sie an eine Eigenschaft eines Objekts etwas zuweisen, wird die Eigenschaft immer im Objekt gespeichert. Sollte sie auch im Prototypen vorhanden sein, wird der Wert aus dem Prototypen damit verdeckt.

Sie können den Prototyp eines Objekts auch selbst festlegen. Entweder gleich bei der Erzeugung, dann verwenden Sie Object.create. Oder Sie ändern ihn nachträglich, mit Hilfe von Object.setPrototypeOf. Von letzterem möchten wir allerdings abraten, damit bringen Sie den Optimizer von JavaScript gründlich aus dem Tritt und erhalten eine Zeitstrafe. Eine Besprechung von Object.create und Object.setPrototypeOf ist für den Einstieg zu umfangreich. Lesen Sie sich bei Interesse die verlinkten Artikel durch.

Wenn Sie eine Konstruktorfunktion nutzen, gibt es eine dritte Möglichkeit, um den Prototypen festzulegen. Jede Funktion - tatsächlich, jede! - besitzt eine Eigenschaft prototype. Dies ist standardmäßig ein Objekt ohne Eigenschaften und mit Object.prototype als Prototyp. Es wird verwendet, wenn Sie die Funktion zusammen mit dem new-Operator als Konstruktor verwenden, um den Prototyp des neuen Objekts festzulegen.

Konstruktor mit Prototyp
function Person (firstName, lastName, age) {
  this.firstName = firstName
  this.lastName = lastName
  this.age = age
}

// Prototypische Personen werden nicht polizeilich gesucht.
Person.prototype.polizeilichGesucht = false;

// sayName Methode für alle Person-Objekte
Person.prototype.sayName = function () {
   if (this.polizeilichGesucht)
      return "Ich bin auf der Flucht und sage nicht, wie ich heiße!";
   else
      return (`Ich heiße ${this.firstName} ${this.lastName} und bin ${this.age}!`)
};

// Erzeuge neue Personen, Anna war unartig!
const anna = new Person('Anna', 'Mustermann', 31);
anna.polizeilichGesucht = true;                // Diese Eigenschaft wird bei anna gespeichert!

const cem = new Person('Cem', 'Mustermann', 2);

console.log(anna.sayName());   // Ich bin auf der Flucht und sage nicht, wie ich heiße
console.log(cem.sayName());    // Ich heiße Cem und bin 2

Dieses Beispiel speichert an Person.prototype zwei Eigenschaften. Zum einen die bekannte sayName-Methode. Sie benutzt this, um auf die firstName und lastName Eigenschaften des Objekts zuzugreifen, für das sie aufgerufen wird. Sie sehen hier noch einmal den Vorteil von this - sayName erfährt so automatisch, für welches Objekt sie ausgeführt wird. Die andere Eigenschaft ist eine Eigenschaft polizeilichGesucht, mit dem Wert false.

Beide Person-Objekten, die danach angelegt werden, erhalten von JavaScript das Person.prototype-Objekt als Prototyp, und bekommen damit sowohl sayName als auch polizeilichGesucht vererbt. Auf diese Weise bekommt jedes Person-Objekt eine Eigenschaft dieses Namens, der den Normalfall für eine Person darstellt. Nur im Ausnahmefall, wie bei Anna, die offenbar eine Packung Windeln für den kleinen Cem geklaut hat, ist es anders.

Mit Prototypen bekommen Sie die Möglichkeit, Objekte mit Eigenschaften und Methoden auszustatten, ohne das jedesmal im Konstruktor tun zu müssen. Wenn Sie Objekte eines bestimmten Typs sehr häufig erstellen müssen, hat das deutliche Vorteile für Verarbeitungsgeschwindigkeit und Speicherbedarf, denn das Zuweisen von Methoden im Konstruktor erzeugt jedesmal eines neues Funktionsobjekt

Die Eigenschaften des Prototyps werden dabei nicht automatisch zu Eigenschaften des neuen Objekts, insofern ist der Prototyp kein Bauplan und der Prototyp wird auch nicht kopiert. Stellen Sie sich das neue Objekt eher wie einen leeren Ordner vor, in dem als letzte Seite ein Blatt mit "Weitere Angaben finden Sie unter ..." geheftet ist. Was Sie in den Ordner einheften, wird so zuerst gefunden. Wenn Sie im Ordner etwas suchen, blättern Sie durch, und wenn Sie nichts finden, stoßen Sie auf das "Siehe unter..." Blatt.

Unterobjekte[Bearbeiten]

Die Idee hinter Objekten ist, dass man Funktionalität und den dafür notwendigen Programmcode kapselt, damit eine inhaltliche Trennung zu anderer Funktionalität möglich ist. Damit sind Objekte auch so etwas wie Unterprogramme, die ihre eigene Datenhaltung haben können. Fangen wir mit letzterem an und schauen, wie man Objekte als Datenbehälter nicht nur für einfache Werte, sondern auch für weitere Objekte einsetzen kann.

Child-object.svg

Die erste, offensichtlichste Implikation ist, dass das innere (Kind)-Objekt in gewisser Weise an das äußere (Eltern)-Objekt gebunden ist. Dies wird als Eltern-Kind-Beziehung bezeichnet, obwohl es in diesem Fall wahrscheinlich zutreffender ist, es als eine Beziehung zwischen Mutter und Baby im Mutterleib zu betrachten. Wenn die Mutter von einem Bus überfahren wird, teilt das Baby ihr Schicksal. Wenn Sie das übergeordnete Objekt löschen, ist auch das untergeordnete Objekt verschwunden. Das Gegenteil ist natürlich nicht der Fall - wenn Sie das Baby-Objekt löschen, ist das Elternobjekt immer noch da, nur eben ohne ein Baby-Objekt (fügen Sie hier eine hitzige politische Diskussion ein).[3]

Eine zweite Folge von verschachtelten Objekten ist ihr Scope oder "Geltungsbereich". Scope bedeutet so viel wie "Ansicht" oder "Kontext", im Sinne von "was von verschiedenen Orten/Punkten aus gesehen oder verstanden werden kann". Um noch einmal die Baby-Metapher aufzugreifen: Das Baby kann in seine Mutter hineinsehen (mit einer Taschenlampe), aber es kann nicht sehen, was seine Mutter sieht.

Der Umzug[Bearbeiten]

Bob und Ann ziehen um. Die "normale" Art des Umzugs besteht jedoch darin, alles in Kartons zu packen und alle Kartons (vielleicht etwas kryptisch) nach ihrem Inhalt zu beschriften. Bei jedem Umzug gibt es eine Box mit der Aufschrift "important stuff" (wichtige Dinge).

Lasst uns ihnen helfen, indem wir ihre Sachen in Boxen packen:

Objekte sind wie Boxen
const importantStuff = {
      money:    500,
      papers:    96;
      annPassport: "er246vjl",
      bobPassport: "kl554mkt"
};

Was wäre, wenn wir etwas hineinlegen, das sich bereits in einer Schachtel befindet - zum Beispiel ein Schmuckkästchen?

Beispiel
importantStuff.jewelryBox = 1;

Das scheint richtig, aber wenn Ann ihre Halskette finden muss, wird sie das können? Wir haben auf jedes Kästchen eine Liste des Inhalts gelegt, aber ihr Halsband wird nicht auf der Liste stehen. Was wir wirklich tun sollten, ist eine Liste dessen zu erstellen, was sich in der Schmuckschatulle befindet, etwas in der Art:

Box in einer Box - ein neues Unterobjekt
importantStuff.jewelryBox = new Object( );
importantStuff.jewelryBox.necklace1 = "Pearl";   // fake
importantStuff.jewelryBox.necklace2 = "Diamond"; // fake


Quellen[Bearbeiten]

  1. Stack Overflow: Should objects be declared const or let?
    tl;dr: Mit const wird festgelegt, dass der Bezeichner (für das Objekt) nicht geändert wird - einzelne Eigenschaften dürfen verändert werden!
  2. freecodecamp: Object Oriented Programming in JavaScript – Explained with Examples by Dillion Megida, 13.02.2020
    Objekte mit Console untersuchen / __proto__
  3. Debreuil: Building Object-Oriented Applications in Flash
    von 2001, aber sehr anschaulich!