JavaScript/Tutorials/OOP/Objekte und ihre Eigenschaften

Aus SELFHTML-Wiki
< JavaScript‎ | Tutorials‎ | OOP(Weitergeleitet von Prototype)
Wechseln zu: Navigation, Suche

Ganz einfach gesagt ist ein JavaScript-Objekt ein Datenspeicherplatz, in dem – im Gegensatz zu einer Variablen – mehr als nur ein Wert abgelegt werden kann. Um diese Werte ansprechen zu können, werden Namen verwendet. Hier unterscheiden sich Objekte von Arrays, die ebenfalls mehr als einen Wert aufnehmen können, aber Indexnummern zur Identifikation benutzen.

Es gibt keine Einschränkung, welche Werte Sie in einem Objekt speichern können. Jeder in JavaScript vorhandene Wertetyp ist zulässig – auch andere Objekte sind möglich!

Aus Sicht eines Programmdesigners sind Objekte Mitspieler in einem großen Team. Jeder dieser Mitspieler hat bestimmte Eigenschaften, und man kann ihm eine Botschaft senden, woraufhin der Empfänger der Botschaft dann etwas tut – und eventuell auch etwas zurückmeldet. Bestimmte Mitspieler sind einander ähnlich: sie besitzen Eigenschaften der gleichen Art, und sie verstehen auch die gleichen Botschaften. Man sagt auch, dass sie zur gleichen Klasse gehören. Eine objektorientierte Software ist dann ein umfangreiches Team solcher Mitspieler, die die Funktionen der Software im Zusammenspiel bereitstellen.

Um diese Designer-Sicht wieder auf eine einfachere Ebene zu bringen: eine Eigenschaft stellt sich in JavaScript als ein Wert dar, der unter einem bestimmten Namen in einem Objekt gespeichert wurde. Ein Nachrichtensystem, um Botschaften zu versenden, gibt es hier nicht (in anderen Sprachen schon). JavaScript löst das einfacher: Sie können Eigenschaften erzeugen, deren Wert eine Funktion ist. Der Aufruf einer solchen Funktion lässt sich als Botschaft an das Objekt verstehen, etwas zu tun. Weil diese Funktion die Methode darstellt, wie das Objekt mit der Botschaft umgeht, werden solche Funktionen auch ganz einfach Methoden genannt.

Man muss dabei im Kopf behalten, dass dies eine sehr einfache Art ist, Botschaften auf Methoden abzubilden. Andere Programmiersprachen legen viel mehr Wert auf die Trennung zwischen Botschaft und Methode, als es JavaScript tut.

Und wie ist das mit den Klassen? Die gibt es in JavaScript eigentlich gar nicht. Statt dessen gibt es Prototypen: sozusagen Kopiervorlagen für Objekte, die ähnlich sein sollen. Dies ist einer der merkwürdigeren Aspekte von JavaScript, und wir werden uns später darum kümmern.

Objekte in JavaScript können auch 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 (propertys) haben, die die Merkmale beschreiben.

Unser erstes Objekt

Es gibt in JavaScript mehrere Möglichkeiten, ein neues Objekt zu erzeugen. Eine davon besteht darin, den Operator new zusammen mit einer sogenannten Konstruktorfunktion zu verwenden. Eine Konstruktorfunktion für einfache Objekte ohne Eigenschaften ist in JavaScript eingebaut und heißt – wenig überraschend – Object:

Erzeugen eines Objekts
const personA = new Object();

Wir haben nun ein neues, leeres Objekt erzeugt und in der Konstanten personA gespeichert. Genau genommen wurde nicht das Objekt gespeichert, sondern nur die Information, wo es im Speicher zu finden ist, denn Objekte sind keine primitiven Werte wie 17 oder false, sondern aufwändigere Gebilde, die von JavaScript eigenständig verwaltet werden.

Vielleicht fragen Sie sich jetzt: „Was soll das denn?“ – denn wir haben personA nicht als Variable, sondern als Konstante deklariert. Das Objekt ist leer, es ist konstant, was soll man damit noch anfangen? Bei const ist es wichtig, zu beachten, was konstant ist. Wir haben in personA einen Verweis auf ein neues Objekt gespeichert. Dieser Verweis ist konstant, wir können in personA keinen anderen Verweis mehr speichern. Das Objekt selbst ist nicht konstant. So etwas geht zwar, das ist aber ein Thema für später.

Warum haben wir const verwendet? Gerade bei Objekten ist es häufig so, dass einer „Variablen“ einmalig ein Objekt zugewiesen wird und die Variable danach nicht mehr verändert werden muss. Durch die Deklaration als const macht man das deutlich und eröffnet der JavaScript-Engine bessere Optimierungsmöglichkeiten[1].

Eigenschaften zuordnen

In vielen Programmiersprachen ist es so, dass Objekte eine Klassendeklaration benötigen, in der festgelegt wird, welche Eigenschaften und Methoden ein Objekt hat. Wie schon erwähnt, ist das in JavaScript anders, es verwendet Prototypen – Kopiervorlagen. Die Konstruktorfunktion Object nutzt als Prototyp ein Objekt ohne Eigenschaften.

Um dem neuen Objekt Eigenschaften zu geben, ist keine spezielle Deklaration erforderlich. Sie können dem Objekt, das in personA gespeichert ist, die Eigenschaften name und alter einfach zuweisen. Solange die Namen dieser Eigenschaften den Regeln für selbstvergebene Namen folgen, fügen Sie dafür an personA einen Punkt und den Namen an:

Zuweisen von Eigenschaften
personA.name = 'Anna';
personA.alter  = 31;

Auf diese Weise werden dem Objekt in personA zwei neue Eigenschaften namens name und alter zugeordnet. Die Eigenschaft name erhält die Zeichenkette 'Anna' als Wert, und die Eigenschaft alter die Zahl 31. JavaScript hat gemerkt, dass es diese Eigenschaften bisher nicht gab, und hat sie dem Objekt automatisch hinzugefügt.

Mögliche Inhalte von Eigenschaften

Die Frage, welche Werte man Eigenschaften zuordnen kann, ist einfach beantwortet: Alle. Alles, was Sie in einer Variablen speichern können, lässt sich auch an eine Eigenschaft zuweisen.

Eigenschaften verwenden

Um den Wert, der in einer Objekteigenschaft gespeichert ist, wieder auszulesen, können Sie ebenfalls die Punktnotation verwenden:

Verwenden von Eigenschaftswerten
alert(personA.name + ' ist ' + personA.alter + ' Jahre alt');

Wie Sie sehen, können Sie personA.name oder personA.alter wie eine Variable verwenden.

Objekte erzeugen

Nach dieser kurzen Tour wollen wir systematischer werden. Um ein neues Objekt zu erzeugen, gibt es grundsätzlich vier Möglichkeiten:

  • Aufruf einer Funktion, die ein neues Objekt zurückliefert. In den Programmierschnittstellen des Browsers finden sich etliche davon (zum Beispiel document.createElement()). Solche Funktionen greifen letztlich aber nur auf eins der folgenden drei Verfahren zurück
  • Verwendung des Operators new zusammen mit einer Konstruktorfunktion. Das haben wir bei unserem ersten Objekt gemacht. Object ist eine grundlegene Konstruktorfunktion, um leere Objekte zu erzeugen, denen man danach eigene Eigenschaften zuweist. Auf das Schreiben eigener Konstruktorfunktionen gehen wir im Abschnitt Konstruktoren ein.
  • Verwendung eines Objektliterals – das zeigen wir als nächstes
  • Verwendung von Object.create – das ist etwas für Fortgeschrittene.

Schauen wir uns nun die Objektliterale an. Unter „Literal“ versteht man einen konstanten Wert, der im Quelltext eines Programms notiert ist, wie 12, 'drei' oder false. So etwas gibt es auch für Arrays (let array = [1, 2, 3];) und für Objekte.

Die beiden folgenden Beispiele erzeugen Objekte mit identischem Aufbau und Inhalt (eine Eigenschaft volljährig ist in JavaScript übrigens überhaupt kein Problem):

Erzeugung mit Schlüsselwort „new“
const personA = new Object();
personA.name       = 'Anna';
personA.alter      = 31;
personA.volljährig = true;
Erzeugung in Literalschreibweise
const personA = {
    name:  'Anna',
    alter: 31,
    volljährig: true,
};

Wenn Sie in JavaScript an einer Stelle, wo ein Wert stehen darf, eine links geschweifte Klammer schreiben, leitet dies ein Objektliteral ein. Innerhalb dieser geschweiften Klammer notieren Sie nun die gewünschten Eigenschaften. Dazu schreiben Sie den Eigenschaftsnamen, einen Doppelpunkt und dann den Wert dieser Eigenschaft. Wenn Sie mehrere Eigenschaften festlegen wollen, trennen Sie sie durch ein Komma. Mit einer rechten geschweiften Klammer schließen Sie das Objektliteral ab.

Es kommt häufig vor, dass man auch hinter der letzten Eigenschaft in einem Objektliteral ein Komma setzt. Früher reklamierte JavaScript das als Fehler, aber mit der Version ES5 (2009) wird es toleriert. Einfach deshalb, weil Generationen von Programmiern geflucht haben, weil sie entweder eine Eigenschaft hinzugefügt, aber kein Komma in der Zeile davor hinzugefügt hatten - oder weil sie die letzte Eigenschaft entfernt hatten, das Komma davor aber nicht.

Hinweis:
Die Literalschreibweise ermöglicht auch das bedarfsweise Erstellen von Objekten, wo mehrere Statements nicht möglich sind (beispielsweise als Funktionsargument). 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

Bitte öffnen Sie das nachfolgende Beispiel in einem neuen Browserfenster. Auf einem Desktop-PC klicken Sie dazu mit der rechten Maustaste auf Vorschau und wählen „Link in neuen Fenster öffnen“. Drücken Sie dann im neuen Fenster die Taste F12 oder die Tastenkombination Strg++I, um die Entwicklerwerkzeuge zu öffnen. Wählen Sie in den Entwicklerwerkzeugen die Seite Konsole (oder Console) aus. Auf dieser Seite sehen Sie, was console.log ausgibt.

Untersuchung mit Console API ansehen …
const personB = {
    name:  'Björn',
    alter: 29,
    hobby: 'Biersorten'
};
    
console.log(personB.name);
console.log(personB.hasOwnProperty('alter'));
console.log(personB);

Dieses erste Live-Beispiel wiederholt den oben vorgestellten Code:

  • Es erzeugt ein Objekt in Literalschreibweise und speichert es in personB.
  • personB.name liest die Eigenschaft name dieses Objekts aus und gibt ihn mit console.log in der Browserkonsole aus.
  • Mit hasOwnProperty('alter') wird überprüft, ob eine solche Eigenschaft existiert. Bei hasOwnProperty handelt es sich um eine Methode, darauf gehen wir im Anschluss weiter ein.
  • console.log(personB) kann nicht nur einfache Werte ausgeben, sondern auch ganze Objekte. Die letzte Zeile des Beispiels tut das.

Sie sollten in der Konsole nun drei Zeilen vorfinden. Wenn Sie auf das graue Dreieck vor Object klicken, öffnet sich eine weitere Ansicht mit allen Eigenschaften dieses Objekts und ihren Werten. Drei davon kennen Sie, aber was hat es mit der vierten Eigenschaft auf sich, die als <prototype>: Object { … } angezeigt wird (in Chrome sind es zwei eckige anstelle der spitzen Klammern)?

OOP-1.png

OOP-2.png

Die Screenshots zeigen, wie die Ausgabe im Firefox-Browser aussieht. Die Beispiele waren bei der Erstellung allerdings noch englisch.

Das, was Sie bei prototype finden, ist der zuvor schon erwähnte Objektprototyp. Er wird hier wie eine Objekteigenschaft mit einem merkwürdigen Namen dargestellt, eine Eigenschaft dieses Namens gibt es aber nicht. Der standardisierte Weg, um den Prototypen eines Objekts heranzukommen, verwendet die Methode getPrototypeOf des Object-Konstruktors.

Die Aussage, dass der Prototyp eine Kopiervorlage sei, stimmt in JavaScript nicht ganz. Tatsächlich gibt es eine dauerhafte Verbindung zwischen Objekt und Prototyp, und wenn Sie eine Objekteigenschaft lesen wollen, die das Objekt selbst nicht besitzt, setzt JavaScript die Suche nach dieser Eigenschaft automatisch im Prototypen fort. Wenn Sie einer Eigenschaft etwas zuweisen, wird dieser Wert aber immer im Objekt selbst gespeichert, auch wenn im Prototypen bereits eine Eigenschaft mit diesem Namen schon existiert. Auf diese Weise entsteht der Anschein einer Kopie.

Wir haben auch erwähnt, dass der Prototyp, der von Object() verwendet wird, keine Eigenschaften besitzt. Wenn Sie das Prototypobjekt in der Konsole aufklappen, finden Sie aber einige, wie zum Beispiel hasOwnProperty. Haben wir gelogen? Nicht ganz. Diese Eigenschaften haben eine Besonderheit: es sind Funktionen – oder Methoden, in der Objektsprechweise. Funktionen, die als Objekteigenschaften gespeichert sind, werden von JavaScript beim Aufruf besonders behandelt.[2]

Sämtliche Objekte in JavaScript, die den Object-Prototypen verwenden (was bedeutet: alle, bis auf die, bei denen Sie es mit Vorsatz verhindern), können diese Methoden nutzen. Eine Übersicht finden Sie bei der Beschreibung von Object unter „Vererbte Methoden“.

Methoden

Wenn es nur darum ginge, einen Container bereitzustellen, in dem man einen Haufen benannter Werte speichern kann, wäre man mit der Objektorientierung schnell fertig. Das Besondere an Objekten ist, dass sie Daten und Funktionalität miteinander verknüpfen. In JavaScript ist das besonders elegant gelöst, denn JavaScript-Funktionen sind selbst Objekte und können damit wie Werte behandelt werden.

Eine Eigenschaft eines Objekts, die einen Funktionsobjekt enthält, nennt man in der objektorientierten Welt eine Methode. Solche Eigenschaften erstellt man am einfachsten durch das Zuweisen einer anonymen Funktionen an den Eigenschaftsnamen, der die Methode bilden soll:

Methode getBeschreibung() ansehen …
personA.getBeschreibung = function() {
  return `Ich heiße ${personA.name} und bin ${personA.alter} Jahre alt!`;
}
    
const personB = {
    name:  'Björn',
    alter: 29,
    hobby: 'Biersorten',
    getBeschreibung: function() {
      return `Mein Name ist ${personB.name} und mein Alter ist ${personB.alter}. Prost!`;
    }
}
    
console.log(personA.getBeschreibung());
console.log(personB.getBeschreibung());

Beide Objekte enthalten nun eine Eigenschaft getBeschreibung, der aber kein Wert, sondern eine Funktion zugewiesen ist. Diese Funktion lässt sich wie jede andere auch aufrufen. Über den Sprachmix im Namen lässt sich trefflich streiten, er hat aber einen Sinn. Diese Methode stellt eine erweiterte Eigenschaft dar, die lediglich eine Information abholt. Solche Methoden werden getter genannt, darum haben wir ein get davor geschrieben. Wir werden später noch eine andere Syntax für getter kennenlernen.

Die merkwürdigen, in `Backticks` eingeschlossenen Zeichenketten, die in den return-Anweisungen verwendet werden, sind Template-Literale. Dabei handelt es sich um eine Variante von Strings, die JavaScript-Ausdrücke enthalten können. Diese Ausdrücke werden in ${…} eingeschlossen und vereinfachen das Zusammensetzen eines Strings aus Bausteinen. Template-Literale werden in dem Moment ausgewertet, wenn der Programmfluss an den Punkt kommt, wo sie stehen. Das Ergebnis ist ein normaler String.

Das umständliche Formulieren einer anonymen Funktion wurde in ECMAScript 2015 für Objektliterale vereinfacht und mit der Syntax für Methoden in class-Definitionen zusammengeführt. Die getBeschreibung-Methode können Sie auch so definieren:

Kurzformat für Methoden in Objektliteralen
const personB = {
   name:  'Björn',
   hobby: 'Biersorten',
   getBeschreibung() {
      return `Mein Name ist ${personB.name} und mein Alter ist ${personB.alter}. Prost!`;
   }

Funktionsparameter für die Methode schreiben Sie einfach in die Klammern hinter dem Methodennamen.


Beide Funktionen beziehen sich auf Eigenschaften ihres Objekts. Das ist nicht wirklich nützlich. Ein Objekt soll für sich stehen, und dann wäre es störend, wenn im Objekt der Name einer Variablen stehen müsste, in der das Objekt gespeichert ist. Und manche Objekte sind überhaupt nicht an Variablen gebunden. Hinzu kommt, dass Methoden auch aus Prototypen geerbt werden können, und irgendwie muss eine geerbte Methode wissen, an welches Objekt sie vererbt wurde.

Gebraucht wird also ein Objekt mit Selbstbewusstsein. Programmcode muss aussagen können: „Lies meine Eigenschaft name“, anstatt wie Julius Caesar von sich selbst in der dritten Person zu sprechen. Microsofts Visual Basic verwendet dafür tatsächlich das Schlüsselwort me, einige andere Sprachen verwenden self, aber JavaScript orientiert sich an C++ und benutzt this.

this

Das Schlüsselwort this verweist in einer Funktion auf den Kontext, in dem sie aufgerufen wurde. Wird eine Funktion als Methode ausgeführt, dann ist dieser Kontext das Objekt, auf dem die Methode aufgerufen wurde.

Rückbezug mit this ansehen …
personA = {};
personA.getBeschreibung = function () {
      return `Mein Name ist ${this.name} und ich bin ${this.alter} Jahre alt!`;
    }
    
const personB = {
    name:  'Björn',
    alter:   29,
    hobby: 'Biersorten',
    getBeschreibung() {
      return `Mein Name ist ${this.name} und ich bin ${this.alter} Jahre alt!`;
    }
}
    
console.log(personA.getBeschreibung());
console.log(personB.getBeschreibung());

Die beiden Methoden enthalten nun anstelle des Objektnamens mit dem Schlüsselwort this einen Bezug auf das aktuelle Objekt. Dadurch wird die enge Koppelung der Methode an die Variable, in der das Objekt gespeichert ist, aufgehoben. Die Bedeutung dieser Trennung werden wir bei der Diskussion von Prototypen kennenlernen.

Vorsicht: this ohne Methodenkontext

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.getBeschreibung() 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 getBeschreibung 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 schreiber = personB.getBeschreibung;
let essay = schreiber();
console.log(essay);      // ???

Auf diese Weise wird personB.getBeschreibung nicht als Methode, sondern wie eine normale Funktion aufgerufen. Was immer nun this während dieses Aufrufes enthält – das Objekt in personB ist es nicht mehr.

Ursprünglich war JavaScript so konzipiert, dass eine Funktion, die nicht als Methode verwendet wird, bei ihrem Aufruf in this das globale Objekt (window-Objekt) vorfindet. Dort gibt es tatsächlich eine Eigenschaft name, aber keine Eigenschaft alter. Die Ausgabe wäre damit „Mein Name ist   und ich bin undefined Jahre alt“. Das kann schwer zu findende Programmfehler hervorrufen - weswegen im Strict Mode, der seit 2010 für alle neuen Programme angeraten ist, der Standardwert für this in einem Funktionsaufruf undefined ist. 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 Methoden als Parameter, wenn dabei der Kontext verloren geht?

Es ist wenig praktikabel, das Problem durch die Übergabe von zwei Parametern zu lösen. Sicher, in PHP hat man das so ähnlich gemacht: man übergibt für diesen Fall ein Array, in dem das Objekt und der Methodenname stehen. Aber das ist PHP, die Sprache, in der der Quirks-Mode der Normalfall ist und die sich nur langsam von den Konzeptunfällen der ersten Jahre erholt. Dort wurde das Problem im Jahr 2016 in Version 7.1 mit Closure::fromCallable sinnvoll gelöst.

Die JavaScript-Lösung für das Problem heißt seit 2009 bind, und sie tut genau das Gleiche wie Closure::fromCallable() in PHP. bind erzeugt eine kleine Adapterfunktion, die sicherstellt, dass eine Funktion exakt im gewünschten Kontext aufgerufen wird.

Erstellen eines bind-Adapters
const personA = {
   name: 'Anna',
   getBeschreibung: function() { return `Ich bin ${this.name}!`; }
};
const beschreibeAnna = personA.getBeschreibung.bind(personA);
console.log('Anna sagt: ', beschreibeAnna());   // Anna sagt: Ich bin Anna

Aufruf einer Methode mit fremdem this

Es kommt auch vor, dass Sie einer Methode ein fremdes this unterschieben möchten. Das macht man beispielsweise bei einigen Methoden von Array-Objekten, die nichts weiter benötigen als eine length-Eigenschaft und einen Indexzugriff und die sich so auch bequem für NodeList-Objekte wiederverwenden lassen.

Zu diesem Zweck stellt Function.prototype zwei Methoden bereit, mit denen man eine Funktion – und damit auch eine Methode – mit einem explizit vorgegebenen Wert für this aufrufen kann. Es handelt sich um die Methoden call und apply. Sie erwarten als ersten Parameter den Wert für this, und danach die Argumente, die der Funktion übergeben werden sollen. Der Unterschied zwischen call und apply ist, dass call diese Argumente als einzeln übergebene Werte erwartet, während apply sie als ein Array haben möchte.

Früher wurde dies sehr häufig für die Array-Methode forEach gemacht, um eine NodeList funktional durchlaufen zu können - 2016 hatte das W3C ein Einsehen und hat eine eigene forEach-Methode für NodeLists spezifiziert.

Aber es gibt auch andere Methoden, deren Übertragung sich lohnt. Beispielsweise würde auch die Array-Methode map mit einer NodeList funktionieren. Ein Beispiel dafür finden Sie auf der Referenzseite von call.

Vorsicht: this und Pfeilfunktionen

Wenn Sie bereits über Pfeilfunktionen gelesen haben, könnten Sie auf den Gedanken kommen, eine solche Pfeilfunktion als Methode nutzen zu wollen. Grundsätzlich geht das, aber dann verhält this sich nicht so, wie Sie es vielleicht erwarten.

Im Artikel über Pfeilfunktionen finden Sie die Aussage, dass this von einer Pfeilfunktion nicht neu gebunden wird. Das bedeutet vor allem, dass eine Pfeilfunktion, die als Methode genutzt wird, in this nicht das Objekt vorgesetzt bekommt, für das der Aufruf erfolgte.

Beispiel
function schreibe(wert) {
   console.log('Hallo Welt, ich bin ein ', wert);
}

let objekt = {
   sagWas: (y) => this.schreibe(y),
   schreibe: function(wert) {
      console.log('Du bist ein ', wert);
   }
}

objekt.sagWas('Depp');

Wenn dieser Code im strict mode ausgeführt wird, wird er mit einem TypeError abbrechen. Im unstrikten Modus wird hingegen „Hallo Welt, ich bin ein Depp“ ausgegeben, und nicht etwa „Du bist ein Depp“. Die Pfeilfunktion, die für die Methode sagWas eingesetzt wurde, behält das this-Objekt bei, das im Moment ihrer Definition galt. Die Zuweisung an die Variable objekt geschah auf globaler Ebene und deshalb wird für this das globale Objekt verwendet, worin die globale Funktion schreibe zu finden ist. statt der Objektmethode aufgerufen wird.




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

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 (vorname, nachname, alter) {
  this.vorname = vorname;
  this.nachname = nachname;
  this.alter = alter;

  this.getBeschreibung = function () {
    return `Mein Name ist ${this.vorname} ${this.nachname} und ich bin ${this.alter} Jahre alt!`;
  };
}

// Neue Personen mit dem `new` Operator erzeugen
const anna = new Person('Anna', 'Mustermann', 31);
const björn = new Person('Björn', 'Mustermann', 29);
const cem = new Person('Cem', 'Mustermann', 2);
    
console.log(ann.getBeschreibung());
console.log(cem.getBeschreibung());

Die Funktion Person nimmt drei Parameter entgegen, deren Werte den Eigenschaften vorname, nachname und alter zugewiesen werden.

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

Beachten Sie: 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.getBeschreibung eine Funktion zu. Funktionen sind Objekte, und jede Funktion trägt den Kontext, in dem sie definiert wurde, mit sich (eine Closure). Das ist aufwändig und kostet Zeit, vor allem, wenn ein Objekt viele Methoden bekommen soll. Geht das nicht besser?

Prototypen

Wir haben schon zu Beginn des Artikels beschrieben, dass Objekte in JavaScript als Kopien eines Prototyp-Objekts erzeugt werden. Hinzu kommen dann die Eigenschaften und Methoden, die direkt in ihm gespeichert wurden.

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 hasOwnProperty 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.

Dass der Prototyp kopiert wird, ist allerdings nicht ganz richtig… Im Abschnitt „Eigenschaften untersuchen“ haben wir bereits gesehen, dass das untersuchte Objekt eine Eigenschaft <prototype> zu besitzen schien. Das ist allerdings keine richtige Eigenschaft, sondern nur ein Hinweis der Entwicklerwerkzeuge auf den verwendeten Prototypen.

Die Wahrheit ist, dass beim Erstellen eines neuen Objekts gar nichts kopiert wird. JavaScript speichert lediglich einen Verweis auf den Prototypen, und wenn man eine Eigenschaft abfragt, die im Objekt nicht existiert, schaut es im Prototypen nach, ob sie dort vorhanden ist. Das dauert zwar ein klein wenig länger, spart aber sehr viel Arbeitsspeicher.

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.

Eine weitere Folge dieser Verweisbildung ist, dass eine Änderung am Prototypen sich sofort in allen Objekten auswirkt, die diesen Prototypen verwenden. Wenn Sie Object.prototype eine weitere Methode hinzufügen, wird diese Methode sofort in allen Objekten sichtbar.

Um den Prototypen eines Objekts obj zu ermitteln, verwenden Sie die Methode Object.getPrototypeOf(obj). Sie liest die interne Eigenschaft des Objekts aus, die den Prototypen speichert, und gibt ihn zurück.

Sie können den Prototyp eines Objekts auch selbst festlegen. Wenn Sie ein neues Objekt mit der Methode Object.create erzeugen, dann können Sie das gewünschte Prototypobjekt als Parameter übergeben. Oder Sie nutzen zum Erzeugen eines Objekts eine Konstruktorfunktion. 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(vorname, nachname, alter) {
  this.vorname = vorname
  this.nachname = nachname
  this.alter = alter
}

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

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

// 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.getBeschreibung());   // Ich bin auf der Flucht und sage nicht, wie ich heiße
console.log(cem.getBeschreibung());    // Ich heiße Cem und bin 2

Dieses Beispiel speichert an Person.prototype zwei Eigenschaften. Zum einen die bekannte getBeschreibung-Methode. Sie benutzt this, um auf die vorname und nachname Eigenschaften des Objekts zuzugreifen, für das sie aufgerufen wird. Sie sehen hier noch einmal den Vorteil von this - getBeschreibung 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 getBeschreibung 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 vielleicht eine Packung Windeln für den kleinen Cem geklaut hat, ist es anders.

Die dritte Möglichkeit nennen wir der Vollständigkeit halber, Sie sollten sie aber nur in Ausnahmefällen einsetzen, denn sie bringt den Optimizer von JavaScript gründlich aus dem Tritt und kostet deshalb viel Laufzeit. Es handelt sich um die Methode Object.setPrototypeOf. Hiermit können Sie den Prototyp eines existierenden Objekts nachträglich ändern. Dieses Thema ist für den Einstieg zu umfangreich. Lesen Sie sich bei Interesse die verlinkten Artikel durch.

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 einer 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.

Besonderheiten von Eigenschaften

Dieses Kapitel steht am Ende des Artikels, weil es über einfache Grundlagen bereits hinausgeht. Sie sollten es überfliegen, müssen aber nicht gleich alles verstehen.

Auffinden aller Eigenschaften eines Objekts

Es gibt mehrere Möglichkeiten, die Eigenschaften eines Objekts aufzuzählen. Sie unterscheiden sich darin, ob sie auch den Prototypen einbeziehen, und ob sie nur die aufzählbaren Eigenschaften verwenden (nicht aufzählbare Eigenschaften erzeugt man mit Hilfe von Object.defineProperty).

  • 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

Variable Eigenschaftsnamen

Für unsere Person Anna waren die Eigenschaften name und alter festgelegt. Es gibt aber auch Situationen, in denen man den Namen einer Eigenschaft erst zur Ausführungszeit kennt. Der Name könnte sich beispielsweise in einer Variablen befinden.

JavaScript unterstützt diesen indirekten Zugriff durch den […]-Operator, den Sie vielleicht schon als Index-Operator von Arrays kennen. Bei Arrays würden Sie in die eckigen Klammern eine Zahl schreiben. Bei Objekten setzen Sie dort den gewünschten Eigenschaftsnamen ein:

Eigenschaftsname in einer Variablen
const prop = 'name';
console.log(`Die Eigenschaft ${prop} hat den Wert ${personA[prop]}.`);

Ein praxisnaher Anwendungsfall könnte darin bestehen, dass Sie eine Funktion schreiben möchten, die auf data-Attribute eines HTML Elements zugreift. Diese stehen über die dataset-Eigenschaft aller HTMLElement-Objekte zur Verfügung. Sie finden darin ein DOMStringMap-Objekt, das die data-Attribute des Elements wiederspiegelt.

Wenn Sie dieser Funktion den Namen des data-Attributs, das sie verwenden soll, als Parameter dataName übergeben können möchten, dann können Sie mittels element.dataset[dataName] auf das benötigte Attribut zugreifen.

Eigenschaften mit irregulären Namen

Die Index-Schreibweise macht es auch möglich, einem Objekt Eigenschaften zuzuweisen, deren Name nicht den Regeln für selbstdefinierte Namen folgt.

Eigenschaften mit Index-Notation setzen
personA['letzte Anmeldung'] = new Date('2023-04-15 17:32');
console.log(`Letzte Anmeldung von ${personA.name} war: ${personA['letzte Anmeldung']}.`);
Dem personA-Objekt aus den vorherigen Beispielen wird hier eine Eigenschaft letzte Anmeldung zugewiesen. Ein Leerzeichen ist in einem JavaScript-Namen nicht zulässig, aber mit Hilfe der Index-Notation lässt sich die Eigenschaft erzeugen und später auch wieder abfragen.

Eine Eigenschaft mit dem Namen `letzte Anmeldung` lässt sich auch in einem Objektliteral verwenden. Dazu schreiben Sie den Eigenschaftsnamen einfach in Anführungszeichen:

Objektliteral mit irregulären Eigenschaftsnamen
const personA = {
   name: 'Anna',
   'letzte Anmeldung': new Date('2023-04-15 17:32'),
};

An dieser Stelle begegnen wir übrigens zwei anderen Objekten. Zum einen ist es das Date-Objekt, mit dem JavaScript Datums- und Zeitwerte darstellt. Der Aufruf new Date('2023-04-15 17:32') ruft die Konstruktorfunktion Date auf und übergibt eine Zeichenkette mit Datum und Uhrzeit, woraus JavaScript dann ein Datumsobjekt erstellt, das diesen Zeitpunkt beschreibt. Zum anderen handelt es sich um das console-Objekt, das ein Teil der Browser-Umgebung ist und dessen Eigenschaft log eine Funktion enthält, mit der Sie Nachrichten in das Konsolenfenster der Entwicklerwerkzeuge Ihres Browsers schreiben können. Eine solche Eigenschaft nennt man auch Methode.

Symbole als Eigenschaftsname

JavaScript legte ursprünglich fest, dass Eigenschaftsnamen Zeichenketten sein müssen. Mit ECMAScript 2015 wurde dies um die sogenannten Symbole erweitert. Dabei handelt es sich um spezielle primitive Werte, für die innerhalb einer JavaScript-Anwendung Eindeutigkeit garantiert werden kann.

Objektliteral mit irregulären Eigenschaftsnamen
const nameSym = Symbol('name');
const nameSym2 = Symbol('name');
if (nameSym == nameSym2)  console.log('Dies kommt NIEMALS vor!');

const personA = new Object();
personA[nameSym] = 'Anna';

Symbol('name') erzeugt ein neues, eindeutiges Symbol. Der Parameter, der an die Symbol-Funktion übergeben wird, dient dabei lediglich als Beschreibung, nicht aber, zum den Wert des Symbols festzulegen. Der im Beispiel gezeigte zweite Aufruf erzeugt garantiert ein anderes Symbol. Auf diese Weise bekommt das personA Objekt eine Eigenschaft, auf die nur zugegriffen werden kann, wenn man Zugriff auf das entsprechende Symbol hat.

Bevor Sie das für eine tolle Idee zum Geheimhalten von Eigenschaften halten - es gibt die Methode Object.getOwnPropertySymbols(), und wenn Sie dieser Methode personA übergeben, erhalten Sie ein Array mit allen Symbolen, die personA für seine Eigenschaften verwendet.

Der Sinn von Symbolen liegt anderswo. Es gibt, was Sie auf der Referenzseite zu Symbolen nachlesen können, eine Liste von bekannten Symbolen (well-known Symbols), und Eigenschaften mit einem well-known Symbol als Name können für ein Objekt bestimmte Funktionen der JavaScript Engine aktivieren.

Symbole und variable Eigenschaftsnamen in Objektliteralen

Es wäre merkwürdig, wenn man Symbole und variable Eigenschaftsnamen nur mit der Indexnotation anlegen könnte. Um solche Eigenschaften in einem Objektliteral zu erzeugen, verwenden Sie sozusagen die Indexnotation innerhalb des Objektliterals:

Objektliteral mit irregulären Eigenschaftsnamen
const nameSym = Symbol('name'),
      alterName = 'alter';

const personA = {
   [nameSym]: 'Anna',
   'letzte Anmeldung': new Date('2023-04-15 17:32'),
   [alterName]: 27
};
Hinweis:
Eigenschaften, deren Name in Anführungszeichen gesetzt wurde, können in eckige Klammern gesetzt werden. Erforderlich ist das aber nicht.

Quellen

  1. Stack Overflow: Should objects be declared const or let?
    tl;dr: const besagt, dass der Verweis auf das Objekt unverändert bleibt – 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__