JavaScript
Einbindung in HTML-Seiten
Leseempfehlung: selfhtml Tutorial Einbindung in HTML
Sprachelemente
Leseempfehlung: selfhtml JavaScript Sprachelemente
Objekte in JavaScript
In JavaScript werden keine Klasse benötigt, um Objekte zu erzeugen. Statt dessen können Objekte mit sogenannten Objektliteralen erzeugt werden:
Tip: Führen Sie die folgenden Anweisungen in der Console von Chrome aus und beobachten Sie die Ausgaben:
Das leere Objekt ist das einfachste Objekt:
> var o = {} > o Object {}
Einige Funktionen und Attribute wurden vom Prototypen Object übernommen, wie z. B. die Funktion toString:
> o.toString // Anzeige des Attributs toString (Funktionen sind Attribute) function toString() { [native code] } > o.toString() // Aufruf der Funktion toString "[object Object]"
Objekte ähneln assoziativen Arrays, die Attribute können als Schlüssel-/Wert-Paare verstanden werden. Objekte können auch direkt mit Attributen angelegt werden:
> var susi = {vorname: "Susi", nachname: "Sonne", alter: 23}; > susi.nachname "Sonne"
Attribute (auch ererbte) können geändert werden. Dabei kann der Typ sich ändern. Beispiel: das geerbte Attribut toString wird mit einer Zahl belegt und danach mit einer Funktion. Innerhalb von Objekten gibt es das Schlüsselwort this, mit dem auf die objekteigenen Attribute und Funktionen zugegriffen werden kann.
> susi.toString = 42 // !!! können toString mit irgendwas anderem ersetzen > susi.toString 42 > susi.toString = function() { return this.vorname + " " + this.nachname + " (" + this.alter + ")"; } function() { ... } > susi.toString() "Susi Sonne (23)"
Objekte mit gleichen Eigenschaften
Häufig werden in Informationssystemen mehrere Objekte desselben Typs verwendet. Die Objekte haben denselben Satz an Attributen und dieselben Funktionen. In anderen Sprachen wie z. B. Java spricht man von Klassen.
Gleichartige Objekte können in Javascript auf verschiedene Weise erzeugt werden. In Java dagegen können Objekte nur erzeugt werden, indem der Konstruktor einer Klasse aufgerufen wird.
Klonen vorhandener Objekte
Mit der for-in-Schleife kann über Attribute von Objekten iteriert werden. Auf die Werte der Attribute kann mittels eckigen Klammern [] wie in einem Array zugegriffen werden:
> for (var attr in susi) {console.log(attr + ": " + susi[attr]);} vorname: Susi nachname: Sonne alter: 23 toString: function () { return this.vorname + " " + this.nachname + " (" + this.alter + ")"; }
Diese Möglichkeit lässt sich nutzen, um Objekte rekursiv zu kopieren:
function clone(obj) { // testen, ob es sich um ein Objekt handelt // Abbruchbedingung der Rekursion if ((obj === null) || (typeof obj !== "object")) { // nein: den Wert einfach zurückgeben return obj; } // sonst: Klon dieses Objekts anlegen var newObj = {}; // Über die Attribute iterieren und diese clonen // Rekursionsschritt for (var attr in obj) { newObj[attr] = clone(obj[attr]); } return newObj; }
Zur Demonstration kann das Objekt susi um ein weiteres Attribut, adresse, das selbst wieder ein Objekt ist, erweitert werden:
> susi.adresse = {strasse: "Friedrich-Heinrich-Allee 25", plz: "47475", ort: "Kamp-Lintfort"} Object {strasse: "Friedrich-Heinrich-Allee 25", plz: "47475", ort: "Kamp-Lintfort"}
Wenn die Funktion clone und alle anderen Anweisungen in einem <script>-Element einer HTML notiert werden, kann diese Seite in Chrome geladen und die Funktion clone im Debugger beobachtet werden. Es zeigt sich, dass die Rekursion bei den primitiven Attributen vorname, nachname und alter in der Abbruchbedingung endet, beim Attribut adresse die Schleife jedoch für dessen Attribute strasse, plz und ort erneut ausgeführt wird. Im Ergebnis entsteht ein neues Objekt als echte Kopie des ursprünglichen Objekts susi:
> var willy = clone(susi); > willy Object {vorname: "Susi", nachname: "Sonne", alter: 23, adresse: Object, toString: function}
An dieser Stelle fällt auf, dass das Objekt willy eine echte Kopie des Objektes susi ist. Zur Erzeugung strukturell gleicher, aber inhaltlich verschiedener Objekte ist das Klonen daher eher unvorteilhaft, da alle Attribute explizit von außen neu gesetzt werden müssen:
> willy.vorname = "Willy" > willy.nachname = "Winzig" > willy.alter = 21 > willy.adresse.strasse = "Marie-Curie-Straße 1" > willy.adresse.plz = "47533" > willy.adresse.ort = "Kleve" > willy Object {vorname: "Willy", nachname: "Winzig", alter: 21, adresse: Object, toString: function}
Prototypen
Ein Objekt lässt sich als Prototyp eines anderen Objektes festlegen. Dazu wird das Attribut prototype, über das jedes Objekt verfügt, gesetzt. Hier entsteht im Gegensatz zum Klonen eine Vererbungshierarchie.
function extendObject(obj) { function C() {} C.prototype = obj; return new C(); }
Ein neues Objekt lässt sich nun unter Verwendung eines bestehenden Objektes erzeugen:
> var willy = extendObject(susi) > willy.toString() "Susi Sonne (24)"
Auch hier lassen sich jedoch keine Attributwerte übergeben, sondern müssen im Nachgang explizit gesetzt werden. Strukturelle Änderungen am originalen Objekt (dem Prototypen) wirken sich direkt auf alle Objekte aus, die auf dem Prototypen basieren:
> willy.vorname = "Willy" > willy.toString() "Willy Sonne (24)" > susi.increaseAlter = function() {this.alter++;} > willy.increaseAlter() > willy.toString() "Willy Sonne (25)"
Erzeugerfunktion
Wird die Erstellung mittels Objektliteral in einer Funktion gekapselt, spricht man von einer Erzeugerfunktion:
function createPerson(vorname, nachname, alter) { return { vorname: vorname, nachname: nachname, alter: alter, toString: function() { return this.vorname + " " + this.nachname + " (" + this.alter + ")"; } }; }
Dieser Funktion lassen sich Attributwerte als Parameter übergeben, jedoch entsteht keine Vererbungshierarchie - es sei denn, es wird auch ein Parameter für das Attribut prototype übermittelt und in der Funktion gesetzt.
Das Objekt wird dann mittels Funktionsaufruf erzeugt:
> var susi = createPerson("Susi", "Sonne", 24) > susi Object {vorname: "Susi", nachname: "Sonne", alter: 24, toString: function} > susi.toString() "Susi Sonne (24)"
Konstruktorfunktion
Konstruktorfunktionen kommen strukturell Java-Klassen mit Konstruktoren am nächsten. Sie werden üblicherweise entsprechend der gängigen Namenskonventionen in CamelCase, beginnend mit einem Großbuchstaben, benannt. Konstruktorfunktionen werden üblicherweise in eigene .js-Dateien ausgelagert; im folgenden Beispiel also Person.js. Der Konstruktor liefert das erzeugte Objekt implizit als Rückgabewert, eine Angabe von return this; ist also nicht erforderlich:
function Person(vn, nn, alter) { // Attribute setzen this.vorname = vn; this.nachname = nn; this.alter = alter; // Methoden this.toString = function() { return this.vorname + " " + this.nachname + " (" + this.alter + ")"; }; }
Der Aufruf der Konstruktorfunktion mit dem Schlüsselwort new führt dann zur Objekterzeugung:
> var willy = new Person("Willy", "Winzig", 21) > willy.toString() "Willy Winzig (21)"
Schutz vor fehlerhafter Verwendung
Bei der Verwendung von Konstruktorfunktionen ist darauf zu achten, dass diese prinzipiell auch ohne new aufgerufen werden können. Dann wird das globale Objekt (im Browser: window) um die aufgeführten Attribute erweitert, aber kein neues Objekt erzeugt. Es ist ein typischer Fehler, dass new vergessen wird:
> var alex = Person("Alex", "Schmidt", 31) > alex undefined > window.vorname "Alex"
Dies lässt sich durch eine Schutzmaßnahme im Konstruktor selbst verhindern:
function Person(vn, nn, alter) { // Welchen Typ hat this? // Schützt den Konstruktor vor Aufruf ohne new // ggf. erfolgt einmalig rekursiver Aufruf des // Konstruktors mit new if (!(this instanceof Person)) { return new Person(vn, nn, alter); } // Attribute setzen this.vorname = vn; this.nachname = nn; this.alter = alter; // Methoden this.toString = function() { return this.vorname + " " + this.nachname + " (" + this.alter + ")"; }; }
Der Aufruf ohne new führt nun dazu, dass der Konstruktor sich selbst korrekt mit new aufruft und das neu erzeugte Objekt zurückliefert:
> var alex = Person("Alex", "Schmidt", 33) > alex Person {vorname: "Alex", nachname: "Schmidt", alter: 33, toString: function}
Nichtsdestotrotz sollte beim Programmieren darauf geachtet werden, stets new zu verwenden, um Problemen mit Konstruktorfunktionen, die nicht über einen derartigen Schutz verfügen, vorzubeugen.
Sichtbarkeit von Attributen
Alle mit this. notierten Attribute und Funktionen sind öffentlich zugänglich. In Konstruktorfunktionen lassen sich auch private Attribute und Attribute angeben, indem this. weggelassen wird. Statt dessen werden lokale Attribute mit var definiert:
function Person(vn, nn, alter) { ... this.toString = function() { count(); return this.vorname + " " + this.nachname + " (" + this.alter + ")"; }; // privater Zähler mit Zählfunktion var counter = 0; var count = function() { counter++; }; // öffentlicher lesender Zugriff this.getCounter = function() { return counter; }; }
Fazit Objekterzeugung
Wegen der genannten Probleme bei den anderen Varianten sind Erzeugerfunktionen und Konstruktorfunktionen zu bevorzugen, wobei die Konstruktorfunktion für fachlich komplexe Objekte möglicherweise bessere Übersichtlichkeit bietet.
Tip zur Verwendung von Testdaten
Testdaten lassen sich im Quellcode ganz einfach als Variable definieren:
// Array von Personen var personen = [ { vorname: "Susi", nachname: "Sonne", alter: 24, adresse: { strasse: "Friedrich-Heinrich-Allee 25", plz: "47475", ort: "Kamp-Lintfort" } }, { vorname: "Willy", nachname: "Winzig", alter: 21, adresse: { strasse: "Marie-Curie-Straße 1", plz: "47533", ort: "Kleve" } } ]; > personen[0] Object {vorname: "Susi", nachname: "Sonne", alter: 24, adresse: Object}
Eventhandling
Die im 1. Semester entwickelten Java-Konsolenprogramme haben als Einstiegspunkt eine main Methode und laufen ohne größere Unterbrechungen bis zum Ende durch. Dabei wird eine Berechnung o. ä. ausgeführt und das Ergebnis ausgegeben. Das folgende UML-Sequenzdiagramm illustriert diesen Ablauf beispielhaft:
Programme mit grafischen Benutzeroberflächen kombinieren solche fachlich eher kleinen Programmteile zu größeren Applikationen, die mit dem User interagieren. Diese Programme laufen üblicherweise nicht permanent, sondern reagieren auf Benutzereingaben. Damit eine Benutzereingabe die Ausführung von Programmteilen (z. B. eine Suche) auslösen kann, muss es ein Stück Programmcode geben, das ausgeführt wird, sobald die Benutzeraktion eintritt. Das Programm "wartet" gewissermaßen andauernd darauf, dass der Benutzer Ereignisse auslöst. Üblicherweise ist die Runtime der Programmteil, der auf die Benutzeraktionen "wartet". Die Programmteile, die auf die Benutzeraktionen mit einer fachlichen Funktion reagieren nennt man Eventhandler. Ein Eventhandler ist eine Funktion, die von der Runtime (dem Browser) aufgerufen wird, sobald die zugehörige Benutzeraktion (z. B. Mausklick) eingetreten ist. Im folgenden Beispiel sind die Funktionen 1, 2 und 3 Eventhandler, die von der Runtime asynchron aufgerufen werden, sobald ein Ereignis eintritt. Bei einem asynchronen Aufruf wartet der Aufrufer nicht darauf, dass der Aufruf zurückkehrt:
Damit die Runtime den korrekten Eventhandler aufrufen kann, muss der Eventhandler zuerst bei der Runtime registriert werden. Im Browser kann die Registrierung von Eventhandlern auf verschiedene Arten erfolgen. Für die Registrierung eines Eventhandlers sind diese Informationen erforderlich:
- Das Ereignis, auf das reagiert werden soll (z. B. Mausklick)
- Das Oberflächenelement, für das das Ereignis überwacht werden soll (das Event-Target)
- Der Eventhandler, eine Funktion, die nach dem Eintritt des Ereignisses ausgeführt wird
Das Prinzip nennt man ereignisgesteuerte Programmierung. Es ist die konzeptionelle Grundlage nahezu aller interaktiven Programme mit grafischer Benutzeroberfläche. Später werden wir sehen, dass nicht nur Benutzeraktionen Ereignisse auslösen können, die durch Eventhandler behandelt werden. Weitere Beispiele sind Netzwerkverbindungen: Wenn Anfragen (Requests) über das Netzwerk abgesendet werden, wartet das Programm meist nicht auf die Serverantwort, sondern registriert einen Handler, der aufgerufen wird, sobald die Antwort des Servers eintrifft (sog. Callback).
Das vollständige Beispiel finden Sie in eventhandler.html.
Registrierung des Eventhandlers HTML-Element über das Attribut oneventname (hier onclick):
... <div id="d1" onclick="var s='Hallo';alert(s);">Ausgabe Hallo direkt</div> <div id="d2" onclick="clickD2();">Ausgabe Hallo Funktion</div> <script> function clickD2() { var s="Hallo"; alert(s); } </script>
Im Attribut onclick des Elements wird JavaScript-Code notiert. Dabei kann es sich sowohl direkt um den fachlichen Code (siehe d1) als auch um einen Funktionsaufruf (siehe d2) handeln.
Registrierung mittels DOM-Manipulation über die Funktion addEventListener:
... <div id="d3">Message über benannten Eventhandler</div> <div id="d4">Message über anonyme Funktion</div> <script> document.getElementById("d3").addEventListener("click", clickD2); document.getElementById("d4").addEventListener("click", function(){ var s = "Hallo"; alert(s); }); </script>
Im ersten Fall wird die Funktion clickD2 aus dem vorherigen Codebeispiel als Eventhandler für das div d3 registriert. Hinter dem Funktionsnamen dürfen keine runden () Klammern notiert werden, da das Funktionsobjekt selbst übergeben werden soll. Mit runden Klammern würde statt dessen die Funktion clickD2() aufgerufen werden und deren Rückgabewert (in diesem Fall undefined) als Eventhandler registriert werden.
Im zweiten Fall (d4) wird eine anonyme Funktion als Eventhandler registriert. Zu beachten sind hier die korrekte Klammerung der in den Aufruf von addEventListener eingeschachtelten Funktionsdefinition sowie das Semikolon am Ende des Aufrufs.
Falls die beim Registrieren eingebundene Funktion selbst kein Eventhandler ist, sondern einen zurückliefert, kann natürlich auch ein Funktionsaufruf erfolgen:
function clickD3() { alert("Aufruf clickD3"); return function() { alert("generierter Eventhandler"); }; }
Die Registrierung erfolgt dann mit dem Handler, der vom Funktionsaufruf clickD3() zurückgeliefert wird (beachte die runden Klammern hinter clickD3):
document.getElementById("d3").addEventListener("click", clickD3());
Der erste Parameter eines Eventhandlers ist standardmäßig das Eventobjekt. Wenn der Eventhandler mit einem Parameter definiert wird, kann darüber auf die Eigenschaften des Events zugegriffen werden. Dies kann z. B. für Spiele oder Grafikeditoren sinnvoll sein:
document.addEventListener("click", function(e){ alert("x: " + e.clientX + " / y: " + e.clientY); });
Events lassen sich also auch direkt mit dem gesamten Dokument als Event Target registrieren, nicht nur für einzelne HTML-Elemente.
Es können auch mehrere Events an demselben Element registriert werden und es können mehrere Eventhandler durch ein und dieselbe Benutzeraktion ausgelöst werden. Beispielsweise wird jeder Klick auf eines der div-Elemente d1 bis d4 dazu führen, dass auch der Eventhandler für den Click am Dokument ausgeführt wird.
JSON - JavaScript Object Notation
JSON ist eine als Zeichenkette serialisierte Darstellung eines Objektes, die menschenlesbar ist und zum Datenaustausch verwendet werden kann. Das Format ist interoperabel. Es ist nicht auf JavaScript beschränkt, es lassen sich beispielsweise auch JSON-Objekte zwischen PHP Skripten austauschen oder zwischen einem JavaScript und einem PHP-Programm.
Eine JSON-Zeichenkette lässt sich aus einem Objekt mittels JSON.stringify() erzeugen:
> var bm = {name: "Bohrmaschine", preis: 73.99} > bm Object {name: "Bohrmaschine", preis: 73.99} > var json = JSON.stringify(bm) > json "{"name":"bohrmaschine","preis":73.99}"
Mit der Funktion JSON.parse() lässt sich auf dem umgekehrten Weg ein JavaScript-Objekt aus der JSON-Zeichenkette erezugen:
> var bm2 = JSON.parse(json) > bm2 Object {name: "Bohrmaschine", preis: 73.99}
JSON-Zeichenketten enthalten nur die datenartigen Attribute der Objekte. Funktionsattribute werden nicht in die Zeichenkette kodiert:
> bm.toString = function() {console.log(this.name + " " + this.preis);} > bm.toString() bohrmaschine 73.99 > var json = JSON.stringify(bm) > json "{"name":"bohrmaschine","preis":73.99}" > var bm2 = JSON.parse(json) > bm2.toString() "[object Object]"
Die Funktion toString ist in der JSON-Zeichenkette nicht enthalten. Statt dessen wird die geerbte Funktion toString des Objekts Object ausgeführt.
Attribute, die selbst Objekte sind, und Arrays werden rekursiv in die JSON-Zeichenkette eingebaut:
> var ww = {name: "Wasserwaage", preis: 25.00} > var bestellung = {datum: "21.05.2017", kunde: "Susi Sonne", artikel: [bm, ww]} > bestellung Object {datum: "21.05.2017", kunde: "Susi Sonne", artikel: Array(2)} > var json = JSON.stringify(bestellung) > json "{"datum":"21.05.2017","kunde":"Susi Sonne","artikel":[{"name":"bohrmaschine","preis":73.99},{"name":"Wasserwaage","preis":25}]}"
Workaround für nicht eingebundene Funktionen
Falls es erforderlich ist, dass Objekte aus JSON-Zeichenketten zusammen mit den zugehörigen Funktionen wiederhergestellt werden, kann ein Workaround mit einer Erzeugerfunktion implementiert werden. Dieser Workaround erfordert die Kenntnis der ursprünglichen Konstruktorfunktion des Objekts:
Die Funktion Bestellung.createFromJSON kann nun verwendet werden, um aus JSON-Zeichenketten, die Bestellung-Objekte enthalten, wieder "echte" Bestellung-Objekte zu erzeugen. Dieses Verfahren funktioniert nur bei überschaubarer Komplexität der betroffenen Objekte und bei Zugang zu den Konstruktorfunktionen.
Beispiel zur Verwendung (Laden Sie die Datei json.html im Browser und führen Sie die folgenden Anweisungen in der Konsole aus):
> var bm = new Artikel("Bohrmaschine", 73.99) > bm.toString() "Bohrmaschine 73.99" > var ww = new Artikel("Wasserwaage", 25) > ww.toString() "Wasserwaage 25" > var b = new Bestellung("21.05.2017", "Willi Wacker") > b.addArtikel(bm) > b.addArtikel(ww) > b Bestellung {datum: "21.05.2017", kunde: "Willi Wacker", artikel: Array(2), addArtikel: function} > var js = JSON.stringify(b) > js "{"datum":"21.05.2017","kunde":"Willi Wacker","artikel":[{"name":"Bohrmaschine","preis":73.99},{"name":"Wasserwaage","preis":25}]}" > var b2 = JSON.parse(js) > b2 Object {datum: "21.05.2017", kunde: "Willi Wacker", artikel: Array(2)}
Das aus der JSON-Zeichenkette neu erstellte Objekt enthält keine Funktionen. Der Aufruf von addArtikel schlägt fehl:
> b2.addArtikel(ww) VM203:1 Uncaught TypeError: b2.addArtikel is not a function at <anonymous>:1:4 (anonymous) @ VM203:1
Werden dagegen über Bestellung.createFromJSON() die ursprünglichen Konstruktorfunktionen verwendet, sind auch die Funktionen vorhanden und können aufgerufen werden:
> var b2 = Bestellung.createFromJSON(js) > b2.addArtikel(ww) > b2 Bestellung {datum: "21.05.2017", kunde: "Willi Wacker", artikel: Array(3), addArtikel: function}
DOM Manipulation mit jQuery
jQuery ist ein JavaScript-Framework, das die DOM-Manipulation, insbesondere die Änderung von Darstellungseigenschaften der Oberflächenelemente stark vereinfacht.
Mit jQuery lassen sich mehrere Elemente selektieren, die modifiziert werden sollen. Die Beispiele beziehen sich auf den folgenden HTML-Ausschnitt aus jquery.html:
<style> .rot {color: red;} .gruen {color: green;} .blau {color: blue;} </style> ... <div id="d1">Bohrmaschine</div> <div id="d2">Binford Tools</div> <div id="d3">73.99</div> <p id="p1">Wasserwaage</p> <p id="p2">Wasserversorgung Kamp-Lintfort</p> <p id="p3">25.00</p>
Die Aufrufe des jQuery-Frameworks beginnen meist mit einem Aufruf des Selektors. Der Selektoraufruf folgt der Syntax $("selektor") wobei selektor ein beliebiger CSS-Selektor ist. Als Ergebnis des reinen Selektoraufrufs wird ein Array mit allen selektierten Elementen zurückgegeben:
> $("#d1") [div#d1] > $("div") (3) [div#d1, div#d2, div#d3, prevObject: jQuery.fn.init(1)]
Auf den Ergebnissen lassen sich direkt weitere Aktionen zur DOM-Manipulation ausführen:
> $("#d1").addClass("rot"); [div#d1.rot] > $("div").addClass("gruen"); (3) [div#d1.rot.gruen, div#d2.gruen, div#d3.gruen, prevObject: jQuery.fn.init(1)]
Neue Elemente lassen sich mit appendin das DOM einfügen:
> $("#p3").append("<span>Ich bin neu hier</span>") [p#p3] > $("#p3 span") [span, prevObject: jQuery.fn.init(1)] > $("#p3 span").attr("id", "s1") [span#s1, prevObject: jQuery.fn.init(1)] > $("#s1").addClass("blau") [span#s1.blau]