06: JavaScript
Wir sprechen hier immer über JavaScript, aber was ist JavaScript überhaupt? Woher kommt es? Und vor allem, was hat es eigentlich mit Websites zu tun?
Inhalt
Was ist die Geschichte von JavaScript?
Um das Internet dynamischer zu machen, wollten die Entwickler des damals immer populärer werdenden Browsers Netscape Navigator 1995 eine Skriptsprache finden, die sich durch ihre Einfachheit gegenüber etablierten Programmiersprachen, ihre Integration mit dem DOM, und ihren kleinen Footprint auszeichnete.
Es sollte eine Programmiersprache nicht für Programmierer sein, sondern für eine damals noch recht neue Kategorie: Designer. Das war die Idee hinter Mocha, einer einfach zu erlernenden Skriptsprache für Nicht-Programmierer. Es fand sich auch schnell ein Programmierer, der bereit war, diese Idee umzusetzen: Brendan Eich, der Vater von JavaScript.
Eich stand aus verschiedenen Gründen unter hohem Zeitdruck, aber er schaffte es. Innerhalb von nur 10 Tagen schrieb er die erste Version von Mocha – umbenannt in LiveScript und später dann zu JavaScript, weil die Syntax so sehr dem damals sehr populären Java ähnelte.
Eigentlich sollte JavaScript Java nicht so ähnlich sein – und wer beide Sprachen beherrscht weiß, dass sie grundverschieden sind und sich lediglich die Syntax ähnelt – aber Marketing wollte es so und deshalb wurde es so umgesetzt auch wenn es retrospektiv vielleicht anders besser gewesen wäre.
Es dauerte Jahre, bis JavaScript sich von dem Stigma als „kleiner Bruder Javas“ lösen konnte und auch heute noch gibt es Entwickler, die JavaScript nicht als eigenständige Programmiersprache ansehen, obwohl es durch die konstante Weiterentwicklung und die Befreiung aus dem Browser definitiv zu einer solchen geworden ist. Dennoch führt diese bewegte Geschichte zu einigen Eigenheiten der Sprache, die sie von anderen Programmiersprachen abgrenzt und zu etwas sehr Besonderem macht.
Dieser Artikel, geht weiter ins Detail, falls ihr interessiert seid.
Erklärung: Was hat es mit diesem ECMA-Script auf sich?
Da JavaScript ein offener Standard werden musste, um in allen möglichen Browsern zu funktionieren, konnte man nicht weiter den Namen JavaScript dafür verwenden, weil die Rechte bei Oracle lagen. Deshalb ist der offizielle Name Javascripts „ECMA-Script“, was manchmal einfach als „ES“ abgekürzt wird und vor allem bei neuen Releases der Sprache auftaucht wie ES5 vs ES6, oder ES2017.
Was macht JavaScript besonders?
In JavaScript sind Funktionen keine separaten Konstrukte wie in vielen anderen
Programmiersprachen, sondern ein eigener Typ. Das bedeutet, dass ihr sie
Variablen zuweisen könnt, oder auch von anderen Funktionen als return
-Wert
zurück geben lassen könnt. Wer noch keine andere Programmiersprache beherrscht
findet das wahrscheinlich logisch, aber es ist nicht selbstverständlich. Diese
Tatsache bedeutet auch, dass Funktionale Programmierung (z.B.
Array.forEach(function)
) möglich wird.
Zu viel Verwirrung führt oft auch das sogenannte Prototype-based Object Model,
mit dem Objektorientierte Programmierung in JavaScript umgesetzt werden kann.
Was genau das bedeutet ist für diesen Kurs zu kompliziert und in modernem JS
auch weniger relevant, da man inzwischen Klassen auch über das class
Keyword
definieren kann.
Durch die so kurze Entwicklungszeit hat JavaScript auch einige „Quirks“, wie zum
Beispiel die Tatsache, dass typeof 'Hello World' === 'string'
aber
typeof new String('Hello World') === 'object'
.
Hier findet ihr eine breite Auflistung von solchen Eigenheiten und ihre Erklärungen, unter Anderem:
[] == ![] === true
deshalb so gut wie immer===
in Vergleichen, statt==
!'b' + 'a' + + 'a' + 'a' === 'baNaNa'
NaN !== NaN
[1,2,3] + [4,5,6] === '1,2,34,5,6'
<!-- ein Kommentar
ist ein gültiger JS Kommentar- Uvm. 😉
Da JavaScript viel im Browser ausgeführt wird, und Internetverbindungen nicht immer die schnellsten sind und auch mal ganz unvermittelt abreißen, werdet ihr außerdem immer wieder mit einem Programmierkonzept in Berührung kommen, das sich „Asynchronität“ nennt.
Was bedeutet "asynchron"?
Wie wir gelernt haben, führt der Computer grob gesagt die Zeilen in unserem Programmcode eine nach der anderen aus, so schnell das möglich ist. Was aber, wenn ihr Code erst nach einer gewissen Zeit ausführen möchtet? Oder, wenn ihr erst abwarten müsst, bis euch ein Server oder eine Datenbank Daten zukommen haben lassen?
Dann braucht ihr aynchronen Code, also Code der jetzt aufgerufen, aber erst später beendet werden soll. Traditionell wird das in JavaScript über sogenannte „Callback“-Funktionen gehandhabt. Ihr ruft eine Funktion auf und übergebt dieser eine zweite Funktion, die aufgerufen werden soll, wenn die erste Funktion abgeschlossen ist.
Das einfachste Beispiel ist hier die setTimeout
-Funktion:
// timeout.js
console.log('Skript startet'); // Zeile wird sofort ausgeführt, Ausgabe: 'Skript startet'
setTimeout(() => { // Zeile wird sofort ausgeführt und bekommt eine neue Funktion übergeben
console.log('Timeout callback wurde aufgerufen');
}, 1000); // timeouts werden in Millisekunden angegeben, also wird der Callback hier in 1 sekunde aufgerufen
console.log('Skript beendet'); // Zeile wird sofort ausgeführt, Ausgabe: 'Skript beendet'
Als unerfahrener Programmierer könntet ihr womöglich denken, dass die Ausgabe dieses Codes so aussieht:
'Skript startet'
# 1 Sekunde Pause
'Timeout callback wurde aufgerufen'
'Skript beendet'
Aber da window.setTimeout()
die Ausführung des Skriptes nicht pausiert,
sondern nur eine Funktion registriert, die nach Ablauf der angegebenen
Zeitspanne ausgeführt werden soll, sieht die reale Ausgabe so aus:
'Skript startet'
'Skript beendet'
# 1 Sekunde Pause
'Timeout callback wurde aufgerufen'
Für einfache Anwendungen reichen Callbacks vollkommen aus, aber da Callbacks wiederum Callbacks enthalten können, kann es schnell zu einem Phänomen kommen, das sich „Callback-Hell“ nennt, weil es praktisch unmöglich wird alle möglichen Fehler abzufangen und richtig zu behandeln, wenn erst einmal mehrere Callbacks wiederum eigene Callbacks aufrufen – es ist ja schon beim Lesen schwierig, sich das vorzustellen.
Deshalb wurden in JavaScript sogenannte „Promises“ entwickelt. Das sind spezielle Objekte, die es uns erlauben, darauf zu warten, dass eine Funktion beendet wird und ein Resultat liefert, mit dem wir dann weiterarbeiten können, aber uns auch eine Möglichkeit geben, Fehler sinnvoll abzufangen und auf sie zu reagieren.
Stellt es euch so vor: eure App fragt eine Datenbank an, in der die ToDos für euren Nutzer abgespeichert sind. Die Datenbank gibt eine Promise zurück, in der sie verspricht die angeforderten Daten abzuliefern, sagt aber auch, dass es einen Moment dauern könnte, die Daten zu laden. Ihr habt also in eurem Code nicht gleich die Daten, sondern nur das Versprechen, dass die Daten bald ankommen werden:
// promise.js
const data = new Promise((resolve, reject) => {
// hier werden die Daten geladen, natürlich in diesem nur simuliert
setTimeout(() => {
// innerhalb von Promises muss man resolve aufrufen, statt return,
// um Daten zurückzugeben
resolve(['todo1', 'todo2', 'todo3']);
// falls etwas schief gehen sollte, kann man auch reject aufrufen, um das
//mitzuteilen:
// reject('Es ist ein Fehler aufgetreten');
}, 2000); // sagen wir es dauert 2 Sekunden die Daten zu laden
}).then((promisedData) => console.log(promisedData)).catch(error => console.error(error));
// mit then wird auf resolve gewartet und reagiert, mit catch auf Fehler
console.log(data); // Ausgabe: Promise { <pending> }
// wir wissen die Promise braucht 2000ms zum Ausführen, also warten wir eine mehr
setTimeout(() => console.log(data), 2001); // Ausgabe: Promise { }
Führt ihr diesen Code aus, werdet ihr sehen, dass data
nicht die geforderten
Daten enthält, sondern nur die Promise, dass es Daten geben wird. Die Promise
ist also noch nicht erfüllt, auf Englisch „pending“. Erst nach 2 Sekunden
erscheint unser Array und wenn wir dann noch einmal data
ausgeben lassen,
sehen wir, dass die Promise erfüllt wurde (sie ist nicht mehr „pending“).
Zugriff auf die Daten haben wir aber trotzdem nur innerhalb der then()
-
Funktion.
Ein Vorteil gegenüber Callbacks ist aber neben der besseren Möglichkeit der
Fehlerbehandlung, dass man an eine Promise beliebig viele .then()
-Funktionen
hängen kann, während es immer nur eine Callback-Funktion geben kann. Promises
können auch aneinandergereiht werden, was man chaining
nennt. Ich finde
Promises auch „logischer“ als Callbacks, denn bei einer Promise schreibt man
erst den Code, der eine Weile braucht und dann (then) den Code, in dem man das
Ergebnis verarbeitet.
Promises sind ein sehr fortgeschrittenes Thema in JavaScript und ich kann es euch hier leider nicht vollständig erklären. Am Anfang werdet ihr auch noch recht selten eigene Promises schreiben müssen, aber ihr werdet spätestens wenn es um die Persistenz in euren Datenbanken geht Promises verarbeiten müssen. Auch ist es nützlich zu wissen, was eine Promise ist, wenn man mal in einer Dokumentation darüber stolpern sollte. 😉
Falls ihr euch wirklich näher damit beschäftigen wollt könnt ihr das natürlich wie immer tun. Hier findet ihr eine Reihe von super Artikeln zum Thema.
Async/await – Rettung in der Not
Wenn wir aber nur Promises verarbeiten wollen, gibt es seit neueren Versionen
von JavaScript eine sehr viel einfachere Methode, dies zu tun, nämlich die
Syntax async/await
. Im Hintergrund passiert das gleiche wie mit Promises, aber
für uns wirkt der Code wie normaler, nicht-asynchroner Code. In anderen Worten:
wir „pausieren“ die Ausführung der nächsten Zeilen bis die Promise erfüllt wurde.
// async.js
// Beispielfunktion, die eine Promise zurückgibt
// könnte z.B. von eurer Datenbank-Library kommen
function getData() {
return new Promise((resolve) => {
setTimeout(() => resolve('irgendwelche daten'), 1000);
});
}
// unser code, um mit der Promise zu arbeiten, ohne sie anfassen zu müssen 😉
async function initialise() { // async sorgt dafür, dass wir await innerhalb der Funktion benutzen dürfen
console.log('initialisiere');
const data = await getData(); // await lässt JS so lang warten, bis die Promise von getData() erfüllt ist
console.log(data); // data ist nicht die Promise, sondern der Wert, der resolve() übergeben wurde
console.log('beendet');
}
// async/await kann nicht außerhalb von Funktionen verwendet werden
// deshalb führen wir hier initialise aus
// Initialise gibt auch eine Promise zurück, denn async sorgt automatisch dafür,
// dass die Funktion eine Promise zurückgibt
initialise();
Genau zu verstehen wie async/await
funktioniert ist zu Fortgeschritten, aber
ihr könnt euch folgendes merken:
- Wenn ihr auf asynchrone Daten warten müsst, schreibt
async
vor die jeweilige Funktion, dann könnt ihr die Ausführung des Skripts mitawait
pausieren - Fehler können ganz normal mit
try…catch
behandelt werden await
kann nicht außerhalb einerasync
-Funktion verwendet werdenasync
sorgt dafür, dass die Funktion automatisch eine Promise zurückgibt
Werte vs. Referenzen
Oder: Warum ändern sich alle meine Objekte gleichzeitig?
Wie wir in der letzten Session gelernt haben, sind Objekte sehr besondere Typen in JavaScript, die sich durch die komplette Sprache ziehen. Im Gegensatz zu Strings, Booleans und Numbers können sie nicht nur einen einzigen Wert, sondern viele Werte gleichzeitig beinhalten.
Eine weitere Eigenheit ist, dass sie im Gegensatz zu den anderen Typen nicht als Werte behandelt werden, sondern als Referenzen. Was genau das bedeutet, können wir an diesem Beispiel sehen:
> let a = 1; // a enthält den Wert 1
> let b = a; // b enthält den Wert von a, also auch 1
> a += 1; // wir addieren 1 auf a
> console.log(a);
2
> console.log(b); // b ändert sich nicht, da wir nur bei a addiert haben
1
> const objA = { a: 1 }; // objA enthält eine Referenz auf das neue Objekt { a: 1 }
> const objB = objA; // objB enthält die gleiche Referenz, auf die auch objA zeigt
> objA.a += 1; // wir addieren 1 auf den Wert, der unter dem Key a gespeichert wird
> console.log(objA);
{ a: 2 }
> console.log(objB); // da objA und objB auf das selbe Objekt zeigen, ist die Ausgabe gleich
{ a: 2 }
Wenn wir ein Objekt erstellen und einer Variable zuweisen, wird nicht das Objekt selbst der Variable zugewiesen, sondern eine Referenz zu diesem Objekt, sozusagen seine Adresse im Speicher. Wenn wir dann einer neuen Variable den Wert der Variablen zuweisen, die die Referenz enthält, kopieren wir also nicht das Objekt sondern nur die Referenz!
Konkret bedeutet das für euch, dass ihr ein Objekt manuell kopieren müsst
falls ihr eine Kopie davon braucht. In modernem JavaScript geht das einfach über
den „spread“-Operator ...
:
> const objA = { a: 1 };
> const objB = {...objA}; // erstelle ein neues Objekt mit den Inhalten von objA
> objA.a += 1;
> console.log(objA);
{ a: 2 }
> console.log(objB); // objB ist ein eigenes Objekt, es wird also nicht beeinflusst
{ a: 1 }
Das Gleiche kann auch mit Object.assign()
erreicht werden:
> const objA = { a: 1 };
> const objB = Object.assign({}, objA); // weise die Properties von objA dem leeren Objekt zu und speicher die Referenz zu diesem in objB
Achtung:
Arrays sind auch Objekte! Für sie gilt also ebenfalls, dass sie als Referenz
zugewiesen werden und nicht als Wert. Stellt also auch dort sicher, dass ihr
ein Array von Hand dupliziert (klont), wenn ihr eine Kopie davon braucht. Das
geht ebenfalls über const arrB = [...arrA]
.
Wenn ihr genau hingesehen habt, ist euch noch eine Besonderheit aufgefallen:
in den Beispielen wurden die Objekte als const
deklariert, aber wir konnten
ihre Werte dennoch ändern! Das liegt daran, dass const
nur festlegt, dass der
Wert innerhalb der Variable konstant ist, und da dieser nur eine Referenz ist
und sich diese Referenz nicht ändert, wenn wir Werte innerhalb des Objekts
ändern, ist es vollkommen legitim, die Variable mit const
zu deklarieren.
Was sind Events?
Eine andere Methode mit Code umzugehen, der nicht festen Reihenfolge abläuft, sind die sogenannten Events. Stellt euch vor ihr seid in einem Restaurant und möchtet etwas bestellen. Die Kellner sind aber nicht immer nur an eurem Tisch, sondern kümmern sich um viele gleichzeitig, also hat der Restaurantbesitzer sich etwas Kluges einfallen lassen: wenn ihr bestellen möchtet, drückt ihr einen Knopf an eurem Tisch und der nächste Kellner, der für euren Bereich zuständig ist bekommt eine Benachrichtigung und nimmt eure Bestellung auf, sobald er kann. Ihr habt dem Kellner durch eine Aktion ein Event geschickt, auf das er reagiert.
In JavaScript geht es genauso. Wenn ihr zum Beispiel auf eurer Website einen Button habt, könnt ihr eine Funktion anhängen, die aufgerufen wird, wenn man auf den Button klickt. Diese Funktion nennt man einen Event-Listener, während die Aktion des Klicks das eigentliche Event ist.
Im Browser gibt es Events für alles mögliche, hier findet ihr eine Auflistung fast aller, aber auch in Node gibt es Events und ihr könnt sogar eure eigenen Events erstellen und diese Aussenden, wie wir uns später ansehen werden, wenn wir uns mehr mit Vue.js befassen.
Aber egal wo ihr euch befindet, es läuft immer gleich ab: ihr registriert einen Event-Listener auf einem Objekt oder Element für ein bestimmtes Event und wenn dieses Event dann eintritt, wird die Funktion, die ihr hinterlegt habt ausgeführt.
Wann immer ihr auf bestimmte Ereignisse reagieren wollt, sei es eine Interaktion eures Nutzers, oder das Enden einer Animation, usw. sind Events und Event-Listener die richtige Wahl.
Achtung:
Am Anfang passiert es oft, dass ihr einem Event-Listener nicht eine Funktion,
sondern das Resultat einer Funktion übergebt, weil ihr sie als funktionsName()
registriert, also direkt aufruft. Übergebt die Funktion immer ohne
die Klammern am Ende! Falls eure Funktion Parameter benötigt, verpackt sie in
eine anonyme Funktion: () => funktionsName(param1, param2)
Wie ihr einen Event-Listener registriert unterscheidet sich je nachdem, ob euer Code im Browser ausgeführt wird, oder ihr ein Skript für Node schreibt. Da wir Anwendungen schreiben werden, die im Browser ausgeführt werden und nicht die Zeit haben, alle Methoden anzusehen, werde ich mich auf die Browser-Methode konzentrieren, beachtet also, dass die folgenden Beispiele nur im Browser funktionieren!
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Event Test</title>
</head>
<body>
<button id="button" type="button">Click Me</button>
<script>
// wir brauchen eine Referenz zu dem Button,
// an den wir den Event-Listener hängen wollen
const button = document.getElementById('button');
// ODER: const button = document.querySelector('#button');
// füge button einen Event Listener für 'click' hinzu und führe die Funktion einmal aus
// Event Handler-Funktion Optionen
button.addEventListener('click', () => console.log('hi'), { once: true });
</script>
</body>
</html>
Im obigen Beispiel fügen wir dem Button mit der ID button
einen Event-Listener
hinzu, der auf das 'click'
-Event reagiert und die übergebene Funktion ausführt.
Da die Funktion console.log()
einen Parameter braucht, umgeben wir sie mit
einer anonymen Funktion. Außerdem übergeben wir die Option once
, die besagt,
dass der Event-Listener wieder gelöscht wird, sobald er einmal ausgeführt wurde.
Erklärung: Was ist mit „onclick“?
Manche von euch werden vielleicht schon mit Attributen wie onclick
gearbeitet haben, um Event-Listener hinzuzufügen, oder vielleicht button.onclick
in JavaScript verwendet haben. Diese Methoden sind zwar auch gültig, aber
bieten bei weitem nicht den vollen Funktionsumfang, den ihr mit addEventListener
erreichen könnt. Verwendet also bitte immer Letzteres, wenn ihr in purem JS
arbeitet. In Vue.js gibt es noch einmal eine andere Möglichkeit, die wir dann
näher beleuchten werden, wenn wir uns damit näher befassen.
Natürlich werdet ihr in den seltensten Fällen ein Event nur einmalig ausführen
wollen, weshalb ihr also oftmals auch einfach keine Optionen übergeben könnt.
Wenn ihr euren Event-Listener zu einem späteren Zeitpunkt wieder entfernen
wollen solltet, könnt ihr das mit Hilfe von removeEventListener()
:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Event Test</title>
</head>
<body>
<p>
Klickt den Button innerhalb der ersten 5 Sekunden nach dem Laden und ihr
werdet folgende Ausgabe sehen:
</p>
<pre>
hi
hi again
</pre>
<p>
Nach fünf Sekunden wird die Ausgabe nur noch folgende sein, da der erste
Listener erfolgreich wieder entfernt wurde, der Zweite aber nicht:
</p>
<pre>
hi again
</pre>
<button id="button" type="button">Click Me</button>
<script>
function sayHi() {
console.log('hi');
}
const button = document.getElementById('button');
button.addEventListener('click', sayHi); // WICHTIG: keine Klammern nach sayHi!
button.addEventListener('click', () => console.log('hi again'));
// entferne den Event-Listener nach 5 Sekunden wieder
window.setTimeout(() => {
// removeEventListener muss genauso aussehen wie addEventListener!
button.removeEventListener('click', sayHi);
// removeEventListener funktioniert nicht mit anonymen Funktionen!
// Funktioniert also nicht, da für JS die anonyme Funktion eine Andere ist als bei addEventListener:
button.removeEventListener('click', () => console.log('hi again'));
}, 5000);
</script>
</body>
</html>
Tipp:
Um die Ausgabe dieser Beispiele sehen zu können, ruft in eurem Browser die Entwicklerwerkzeuge auf und blendet die Konsole ein. Unter Chrome geht das mit Rechtsklick irgendwo auf der Seite → „Untersuchen“ und dann Esc, falls die Konsole nicht schon angezeigt wird.
Eine Besonderheit von Events im Browser ist, dass sie standardmäßig „bubblen“,
also vom Ursprungsort immer weiter im DOM-Baum hinaufsteigen, bis sie das
window
-Objekt erreichen. Konkret bedeutet das, dass ihr auch auf Events
reagieren könnt, die innerhalb eines anderen Elements passieren:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Event Test</title>
</head>
<body>
<div class="wrapper">
<button id="button" type="button">Click Me</button>
</div>
<script>
// mit querySelector könnt ihr Elemente wie in CSS auswählen, also . für Klasse z.B.
const wrapper = document.querySelector('.wrapper');
const button = document.querySelector('#button');
// der Event Listener wird aufgerufen, auch wenn wir nur auf den Button klicken
// das Event wird erst beim button aufgerufen, dann beim wrapper, egal in
// welcher Reihenfolge ihr die Listener definiert
wrapper.addEventListener('click', () => console.log('hi from wrapper'));
wrapper.addEventListener('click', () => console.log('hi from button'));
// Ausgabe in der Browser-Konsole nach Klick auf Button:
// hi from button
// hi from wrapper
// Ausgabe in der Browser-Konsole nach Klick auf Wrapper:
// hi from wrapper
</script>
</body>
</html>
Um den Überblick zu behalten, welches Event woher kam, wird eurem Handler automatisch als erster Parameter das Event-Objekt an sich übergeben, welches einige nützliche Informationen enthält:
event.type
: der Typ des Events, z.B. 'click'event.currentTarget
: das Element, das gerade den Handler ausführt, z.B. wrapperevent.target
: das Element, das das Event ausgelöst hat, z.B. button- Uvm. wie die Mausposition in Mouse-Events, die getrückte Taste in Key-Events, etc.
Wenn euch die genauen Inhalte eines Event-Objekts interessieren, könnt ihr es natürlich in DevDocs nachschlagen, oder noch einfacher, euch einfach das Event- Objekt in die Konsole ausgeben lassen:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Event Test</title>
</head>
<body>
<button id="button" type="button">Click Me</button>
<script>
const button = document.getElementById('button');
// ODER: const button = document.querySelector('#button');
// Event-Handler bekommen immer implizit das Event als ersten Parameter
// übergeben, hier habe ich ihn in die Variable "e" gesteckt
button.addEventListener('click', () => console.log(e), { once: true });
// Ausgabe in der Browser-Konsole:
// MouseEvent {…}
</script>
</body>
</html>
Wann immer ihr grafische UIs baut, werdet ihr mit sehr vielen Interaktionen des Nutzers umgehen müssen und Events eignen sich dafür hervorragend. Hier die häufigsten Event-Typen:
'click'
für Mausklicks'mousedown'
wenn eine Maustaste gedrückt wird'mouseup'
wenn eine Maustaste losgelassen wird'mouseenter'
wenn der Mauszeiger über ein Element bewegt wird'mouseleave'
wenn der Mauszeiger ein Element wieder verlässt'keydown'
wenn eine Taste gedrückt wird'keyup'
wenn eine Taste losgelassen wird'touchstart'
wenn ein Touchscreen berührt wird'touchend'
wenn ein Touchscreen-Berührung endet'focus'
wenn ein Element den Focus erhält'blur'
wenn ein Element den Focus verliert
Wie ihr gelernt habt, wird JavaScript zwar inzwischen für alles mögliche verwendet, aber ursprünglich wurde es speziell dafür geschaffen, Webseiten dynamischer zu machen. Deshalb gibt es viele Funktionen in JavaScript, die es euch leicht machen mit dem HTML und darüber auch mit dem CSS eurer Webseiten zu interagieren.
Wie sind JavaScript und HTML verbunden?
Wie ihr noch aus Webdesign I wissen werdet, ist HTML nur eine
Beschreibungssprache für die Struktur von Dokumenten. Ihr beschreibt also die
Struktur eurer Daten und der Web-Browser kümmert sich darum, diese Beschreibung
in die eigentliche Website umzusetzen. Diese virtuelle Datenstruktur nennt man
DOM, oder Document Object Model, und sie repräsentiert euren Einstiegspunkt um
über JavaScript mit eurem HTML interagieren zu können. Das DOM ist ein
sogenannter Node-Baum, eine Datenstruktur in der Informatik, die wie ein Baum
aufgebaut ist: es gibt eine Wurzel und von dort aus breiten sich immer mehr Äste
und Unter-Äste aus, die irgendwann in Blättern enden. In HTML ist die Wurzel das
<html>
-Element, <head>
und <body>
sind die ersten Äste und so weiter.
Das DOM ist eine API, ein „application programming interface“ – so nennt man die Schnittstellen zwischen zwei oder mehr voneinander unabhängigen Bereichen, in unserem Fall HTML und JavaScript. Es gibt auch APIs für den Zugriff auf die Kameras von Geräten, oder einige Funktionen des Betriebssystems. Eine API liefert alle nötigen Funktionen, um mit diesen „externen“ Bereichen zu interagieren.
HTML und JavaScript sind also über die DOM-API miteinander verbunden.
Erklärung: Wie bekomme ich mein JS in HTML?
Wie ihr in den obigen Beispielen schon gesehen habt, könnt ihr in HTML einfach
<script>
-Elemente verwenden, um euer JavaScript direkt in
euer HTML zu schreiben. Für kleinere Beispiele ist das in Ordnung, aber da wir
wissen, dass Ordnung alles ist, macht es Sinn für größere Projekte euer JS in
eigenen Dateien abzulegen und in euer HTML zu importieren, ähnlich wie ihr das
schon mit Stylesheets macht. Dieser Import erfolgt ebenfalls über das
<script>
-Element, nur dass ihr einen Pfad zu eurem Skript
in das src
-Attribut übergebt. Wie ihr JavaScript-Dateien in
andere JavaScript-Dateien importieren könnt, werden wir lernen, wenn wir uns
näher mit Vue.js auseinandersetzen.
Elemente Auswählen
Euer Einstiegspunkt, um HTML-Elemente in JavaScript „auszuwählen“, also eine
Referenz zu ihnen zu erhalten, damit ihr sie verändern könnt, ist immer das
document
-Objekt. Dieses Objekt liefert euch eine Reihe von Funktionen und
Eigenschaften, mit denen ihr weiterarbeiten könnt:
document.documentElement
ist eine Referenz auf euer<html>
-Elementdocument.body
ist eine Referenz auf euer<body>
-Elementdocument.head
ist eine Referenz auf euer<head>
-Elementdocument.querySelector(cssSelector)
wählt das erste Element im Baum aus, auf das der übergebene CSS-Selektor zutrifft und gibt eine Referenz auf das Element zurückdocument.querySelectorAll(cssSelector)
wählt alle Elemente im Baum aus, auf die der übergebene CSS-Selektor zutrifft und gibt einNodeList
-Objekt mit allen Elementen zurück. NodeLists sehen aus wie Arrays, sind aber keine, d.h. ihr könnt mit ihnen nicht wie mit Arrays arbeiten. MitArray.from(NodeList)
könnt ihr eine NodeList in ein normales Array konvertieren, verliert dadurch allerdings die Tatsache, dass sich NodeLists normalerweise in Echtzeit aktualisieren, wenn ihr das DOM verändert!document.getElementById(id)
gibt das Element mit der IDid
zurück, in manchen Fällen ist das schneller alsdocument.querySelector('#id')
, aber wichtig: inquerySelector
-Funktionen müsst ihr die ID wie in CSS mit#
angeben, ingetElementById
ohne#
!
Tipp:
Die Funktionen querySelector
und querySelectorAll
können für jede Element-Referenz im Baum aufgerufen werden, um sie auf die
Kind-Elemente dieses Elements zu beschränken. So könnt ihr zum
Beispiel alle <p>
-Elemente innerhalb eines <article>
-Elements auswählen,
wenn ihr querySelectorAll('p')
auf euerem <article>
aufruft anstatt auf
document
.
Habt ihr erst einmal eine Referenz auf ein Element, gibt es noch weitere Funktionen und Eigenschaften, die ihr verwenden könnt:
element.children
eine NodeList aller Kind-Elemente, d.h. Elemente die sich innerhalb des Elements befindenelement.firstElementChild
das erste Kind-Elementelement.lastElementChild
das letzte Kind-Elementelement.parentElement
das Eltern-Element des Elements, d.h. das Element, dass das referenzierte Element umgibtelement.nextElementSibling
undelement.previousElementSibling
sind die Geschwister-Elemente, also Elemente auf der selben Ebene vor und nach dem referenzierten Element
Erklärung: Was ist der Unterschied zwischen Node und Element?
Im DOM wird zwischen Nodes und Elementen unterschieden, wobei Elemente eine
spezielle Art von Nodes sind. Elemente sind alle Nodes, die ihr auch aus HTML
kennt: <p>
und so weiter, aber spezielle Objekte wie
document
und der Text innerhalb eines Elements sind ebenfalls Nodes.
Prinzipiell werdet ihr anfangs immer eher mit Elementen arbeiten, aber wenn
ihr alle Nodes auswählen wollt, könnt ihr in den Funktionen oben einfach das
„Element“ weglassen, z.B. nextSibling
. Ausnahme:
element.children
wird zu element.childNodes
und
element.parentElement
wird zu element.parentNode
.
Elemente Identifizieren
Wenn ihr einmal eine Referenz auf ein oder mehrere Elemente habt, kann es
vorkommen, dass ihr sie näher identifizieren möchtet. Das könnt ihr mit Hilfe
der tagName
-Eigenschaft. Sie gibt euch immer den Tag des Elements in Versalien
zurück: document.querySelector('p').tagName === 'P'
.
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Identifikationstest</title>
</head>
<body>
<p>Ein bisschen Text</p>
<button type="button">Klick Mich!</button>
<p>Mehr Text</p>
<script>
const button = document.querySelector('button');
button.addEventListener('click', () => {
// Alle Kind-Elemente von Body (p und button)
const allChildren = document.body.children;
// NodeList muss in ein Array konvertiert werden, damit man Array-Funktionen verwenden kann
const childrenArray = Array.from(allChildren);
// Array.filter() gibt ein neues Array zurück, das nur die Elemente enthält,
// für die die Funktion true zurückgibt
const onlyP = childrenArray.filter((el) => el.tagName.toLowerCase() === 'p');
console.log(onlyP);
// Ausgabe:
// [ p {…}, p {…} ]
});
</script>
</body>
</html>
Elemente Verändern
Habt ihr ein Element, könnt ihr es in JavaScript natürlich auf vielfältige Art und Weise verändern. Hier sind die Häufigsten:
element.innerHTML
enthält das komplette HTML innerhalb des Elements als
String. Ihr könnt hier auch einen neuen String zuweisen, um das HTML zu
verändern – passt aber auf es kann sehr unsicher sein, einfach HTML als
Text in ein Element zu schreiben, vor allem, wenn es von einem Nutzer kommt!
// HTML: <p><strong>Fetter</strong> Text!</p>
const p = document.querySelector('p');
console.log(p.innerHTML); // Ausgabe: '<strong>Fetter</strong> Text!'
p.innerHTML = '<em>Kursiver</em> Text!';
console.log(p.innerHTML); // Ausgabe: '<em>Kursiver</em> Text!'
element.innerText
funktioniert wie innerHTML
, aber gibt nur den Text
innerhalb des Elements zurück, ohne etwaiges HTML. Benutzt diese Funktion,
wenn ihr den Text innerhalb eines Elements ändern wollt, bedenkt aber, dass
etwaiges HTML überschrieben wird:
// HTML: <p><strong>Fetter</strong> Text!</p>
const p = document.querySelector('p');
console.log(p.innerText); // Ausgabe: 'Fetter Text!' → das <strong>-Tag wird ignoriert
p.innerText = 'Normaler Text!'; // Das <strong>-Tag geht verloren
console.log(p.innerHTML); // Ausgabe: 'Normaler Text!'
p.innerText += ' Mehr Text!'; // Ihr könnt Text natürlich nicht nur ersetzen, sondern auch anfügen
console.log(p.innerHTML); // Ausgabe: 'Normaler Text! Mehr Text!'
Attribute und Eigenschaften
element.property
enthält den Wert von property
, wenn property
ein
gültiges HTML-Attribut ist, z.B. hat button.id
für <button id="foo">
den
Wert 'foo'
. So könnt ihr beispielsweise auch den Wert eines Input-Elements
auslesen, denn der wird im value
-Attribut abgespeichert:
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Input Test</title>
</head>
<body>
<p class="output">Ausgabe: (Button klicken)</p>
<input type="text" value="Käse">
<button id="clicker" type="button">Ausgeben!</button>
<script>
const output = document.querySelector('.output');
const input = document.querySelector('input');
const button = document.querySelector('#clicker');
// wir hängen an zwei verschiedenen Orten an unseren Output an,
// deshalb eine eigene Funktion dafür – don't repeat yourself!
function addOutput(value) {
output.innerText += `\nAusgabe: ${value}`;
}
button.addEventListener('click', () => {
// wenn der Button geklickt wird, wird der aktuelle Wert des Inputs
// an den Text unseres Output-Elements gehängt
addOutput(input.value);
});
input.addEventListener('keyup', (e) => {
// wenn eine Taste gedrückt wird, schauen wir, ob es Enter war
if (e.key === 'Enter') {
// falls ja, hängen wir den output an, e.target ist das Element,
// auf dem das Event ausgelöst wurde, in diesem Fall also input
// also müssen wir hier gar nicht auf unsere Variable zugreifen
addOutput(e.target.value);
// wir können aber natürlich, wenn wir wollen, sind aber dann darauf
// angewiesen, dass die Variable korrekt deklariert wurde, unsere
// Funktion ist also nicht universell einsetzbar.
// Properties (Eigenschaften) eines Objekts können natürlich auch
// von JS aus geändert werden und wirken sich dann auf das HTML aus
input.value = '';
}
});
</script>
</body>
</html>
element.hasAttribute(name)
überprüft, ob das Element das Attribut mit dem
Namen name
hat.
element.getAttribute(name)
holt sich den Wert des Attributs name
, wie er im
HTML geschrieben ist, also immer als String. Z.B. ist das href
-Attribut
immer die volle URL, wenn man es über element.href
abfragt, aber element.getAttribute('href')
gibt es so zurück, wie es im HTML geschrieben wurde, also z.B. als relativer Pfad.
element.setAttribute(name, value)
erlaubt es euch das Attribut name
mit dem
Wert value
zu versehen, aber benutzt lieber die Schreibweise element.name = value
.
element.removeAttribute(name)
löscht das Attribut name
vom Element.
element.attributes
enthält eine vollständige Liste aller Attribute des Elements.
style
und class
Spezialfälle: Wenn ihr mit dem CSS-Styles eines Elements interagieren wollt könnt ihr entweder
über das style
-Attribut des Elements, oder indem ihr seine Klassen modifiziert.
Der Zugriff auf das style
-Attribut erfolgt über element.style
, gefolgt von
der CSS-Eigenschaft, die ihr verändern wollt. Um einen Text rot zu färben und
die Schriftart zu ändern wäre das zum Beispiel die Folgende:
// p ist eine Referenz auf ein <p>-Element im HTML
p.style.color = 'red';
// Schriftart, 'font-family' in CSS wird in JS in camelCase geschrieben:
p.style.fontFamily = 'sans-serif';
Wollt ihr eine Eigenschaft wieder entfernen, könnt ihr sie entweder auf einen
leeren String setzen, oder mit element.style.removeProperty(property)
wieder
löschen.
Für Klassen gibt es zwei separate Eigenschaften:
element.className
ist der String wie er imclass
-Attribut im HTML steht, wenn ihr ihn ändert hat es den gleichen Effekt, wie wenn ihr ihn direkt im HTML ändern würdetelement.classList
ist ein spezielles Objekt, dass es euch erlaubt, einzelne Klassen hinzuzufügen und wieder zu entfernen, sowie zu überprüfen, ob ein Element eine bestimmte Klasse hat:<!DOCTYPE html> <html lang="en" dir="ltr"> <head> <meta charset="utf-8"> <title>Class Test</title> <style media="screen"> .red { color: red; } .text { font-family: sans-serif; } </style> </head> <body> <p class="red text">Ein bisschen Text</p> <script> const p = document.querySelector('p'); // wenn p nicht die Klasse 'red' besitzt, füge sie hinzu if (!p.classList.contains('red')) p.classList.add('red'); else p.classList.remove('red'); // falls sie existiert, entferne sie // hat den selben effekt wie der Code oben, nur in einer Zeile p.classList.toggle('red'); </script> </body> </html>
Elemente Erstellen und Entfernen
Früher oder später werdet ihr einen Punkt erreichen, an dem es nicht mehr
ausreicht, bestehende Elemente zu verändern. Stattdessen möchtet ihr neue
Elemente erstellen und dem DOM hinzufügen. Auch dafür geben euch das document
-Objekt und andere Nodes nützliche Funktionen an die Hand.
Mit Hilfe von document.createElement(tag)
könnt ihr ein neues Element mit dem
Tag tag
erstellen. So könnt ihr zum Beispiel ein neues Paragraph-Element
erstellen: const p = document.createElement('p');
. Damit enthält die Variable
p
euer neues Element, das ihr wiederum mit den vorhin angesprochenen
Funktionen und Eigenschaften verändern könnt.
Allerdings ist euer Paragraph noch nicht im DOM selbst und wird somit nicht auf der Seite angezeigt. Um Elemente zum DOM hinzuzufügen, benötigt ihr eine der folgenden Funktionen:
node.append(element [, element])
fügt eines oder mehrere Elemente an das Ende vonnode
, wieArray.push()
. Die Elemente sind Kinder vonnode
, also innerhalb vonnode
node.prepend(element [, element])
fügt eines oder mehrere Elemente an den Anfang vonnode
, wieArray.unshift()
. Die Elemente sind Kinder vonnode
, also innerhalb vonnode
node.before(element [, element])
fügt eines oder mehrere Elemente vornode
ein, die neuen Elemente sind Geschwister vonnode
, also außerhalb vonnode
node.after(element [, element])
fügt eines oder mehrere Elemente nachnode
ein, die neuen Elemente sind Geschwister vonnode
, also außerhalb vonnode
node.replaceWith(element [, element])
ersetztnode
durch eines oder mehrere Elemente
Ihr könnt diese Funktionen übrigens auch dazu verwenden, ein Element an eine andere Stelle zu verschieben. Denn sollte es bereits im DOM existieren, entfernt ein Aufruf dieser Funktionen es zunächst und fügt es dann an der neuen Position wieder ein.
Wollt ihr ein Element allerdings nicht verschieben, sondern einfach nur löschen,
braucht ihr nur element.remove()
aufzurufen, um das zu erreichen.
Achtung:
Diese neuen Funktionen werden von Internet Explorer nicht unterstützt – dort
müsst ihr noch die veralteten Funktionen node.appendChild(child)
und node.removeChild(node)
verwenden!
Falls ihr einmal ein bestehendes Element mit allen seinen Attributen duplizieren
möchtet, könnt ihr das außerdem mit node.cloneNode(deep)
erreichen. Je nachdem,
ob deep
true
oder false
ist, werden die Kind-Elemente des Elements
ebenfalls dupliziert oder eben nicht.
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Einfügen, Verschieben, Löschen</title>
</head>
<body>
<!-- Body ist komplett leer, alles wird mit JS generiert -->
<script>
// wir erstellen die Elemente, die wir später benutzen wollen
const h1 = document.createElement('h1');
const ul = document.createElement('ul');
const li = document.createElement('li');
const button = document.createElement('button');
// und deklarieren ein paar Variablen
const lis = [];
let round = 0;
// wir wollen den Text unserer H1 aktuell halten
function updateH1() {
h1.innerText = `Element Tester (Round ${round})`;
}
updateH1(); // wenn wir hier den Text nicht setzen ist die h1 leer
h1.style.fontFamily = 'sans-serif'; // wir können auch gleich den Style anpassen
// hier bearbeiten wir das li
li.innerText = 'Element 1';
lis.push(li); // und fügen es als erstes Element in unsere Liste ein
// selbes Spiel mit dem Button
button.type = 'button';
button.innerText = 'Click Me';
button.addEventListener('click', () => {
// wir holen uns ein Array aller <li>-Elemente innerhalb der <ul>
const remainingLis = Array.from(ul.querySelectorAll('li'));
// wenn es noch mindestens eines gibt, wird es gelöscht
if (remainingLis.length > 0) remainingLis[0].remove();
else { // ansonsten benutzen wir unsere Liste an <li>s, um wieder welche einzufügen
ul.append(...lis); // alle Elemente im Array lis in ul einfügen
round += 1; // es ist eine neue Runde
updateH1(); // der Text von h1 soll auch aktualsiert werden
}
});
// hier generieren wir 4 zusätzliche <li>-Elemente auf Basis des ersten
for (let i = 1; i < 5; i += 1) {
const clone = li.cloneNode(); // klone das erste <li>
clone.innerText = `Element ${i + 1}`; // aktualisiere seinen Text
lis.push(clone); // und stecke es in das lis-Array
}
// ab hier fügen wir unsere Elemente in das DOM ein, vorher existieren sie nur „virtuell“
ul.append(...lis); // erst stecken wir alle lis in ul
document.body.append(ul); // dann packen wir ul in den body
ul.before(h1); // vor ul soll die H1 sein
ul.after(button); // nach ul soll der button sein
</script>
</body>
</html>
Jetzt kennt ihr die Grundlagen der Interaktion zwischen JavaScript und HTML, natürlich ist das Thema (wie immer) noch viel breiter und komplexer als hier dargestellt, aber mit den hier vorgestellten Optionen könnt ihr schon sehr viel erreichen und wir werden in der nächsten Session lernen, wie wir unser Leben durch Frameworks wie Vue.js noch deutlich vereinfachen können. Falls ihr mehr über das heutige Thema erfahren möchtet, kann ich euch wie so oft das zugehörige Kapitel auf javascript.info empfehlen, auf dem ich auch diesen Abschnitt aufgebaut habe.
Praxis
Wie auch schon in der letzten Session, haben wir heute wieder viel Neues kennengelernt, aber wirklich verinnerlichen könnt ihr diese Ideen und Konzepte nur, indem ihr sie selbst ausprobiert und benutzt. Deshalb empfehle ich euch auch diesmal wieder, alle Beispiele noch einmal selbst abzutippen, zu modifizieren und auszuprobieren. Falls dabei irgendwelche Fragen oder Unklarheiten aufkommen sollen, fragt einfach nach. Code-Beispiele für spezifische Fragen sind gern gesehen.
Falls ihr nicht für jedes Beispiel eine neue HTML-Datei anlegen möchtet, oder einfach gerne schnell etwas in eurem Browser ausprobieren möchtet, kann ich euch das Tool JSFiddle ans Herz legen. Dort könnt ihr schnell und einfach HTML, CSS, und JS ausprobieren und eure Ergebnisse sofort sehen, spart euch aber immer DOCTYPE, head, body, etc. angeben und Skripte und Stylesheets einbinden zu müssen. Ihr braucht keinen Account, um das Tool zu verwenden, aber wenn ihr eure Fiddles in der Zukunft wieder Löschen, oder sie nicht öffentlich zugänglich machen wollt, könnt ihr euch natürlich einen Account erstellen.
Zahlenratespiel 2.0
Jetzt ist es endlich an der Zeit, eurem Zahlenratespiel von letzter Session ein
grafisches Interface zu geben. Setzt also bitte euer Ratespiel nun als eine
interaktive HTML-Website um und verzichtet dabei darauf, console.log
zu
verwenden. Stattdessen sollte all euer Output auch in der HTML-Seite auftauchen.
Ihr braucht:
- Einen Bereich für den Output des Computers
- Ein Textfeld, in das der gewünschte Input eingegeben werden und mit Enter bestätigt werden kann. Das Textfeld sollte nach einer erfolgreichen Eingabe wieder leer sein, um die nächste Eingabe aufnehmen zu können
Fleißsternchen gibt es dafür, dass das ganze Spiel auch mit CSS hübsch gemacht wird, z.B. indem das Textfeld rot wird wenn die Eingabe ungültig war, etc. Aber wenn ihr euch vorerst auf den Code konzentrieren möchtet, dann ist das auch in Ordnung.
Weitere Gestaltung
Inzwischen solltet ihr die meisten eurer Screens bereits durchgestaltet haben. Bitte teilt eure Figma-Dateien mit mir, oder schickt mir Bilder / PDFs mit euren Screens, damit ich euch Feedback geben kann. Das ist natürlich weiterhin freiwillig, aber ich rate euch, diese Möglichkeit wahrzunehmen, damit ihr möglichst bald guten Gewissens mit der technischen Umsetzung beginnen könnt.
Jetzt wo ihr praktisch alles wisst, was ihr wissen müsst, um mit HTML und JavaScript zu arbeiten, werden wir uns in der nächsten Session endlich mit dem auseinandersetzen können, was euer Leben stark vereinfachen und eure Entwicklerarbeit deutlich angenehmer machen wird: Frameworks im Allgemeinen und Vue.js im Speziellen. 🎉