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

  1. Was ist die Geschichte von JavaScript?
  2. Was macht JavaScript besonders?
  3. Was bedeutet "asynchron"?
  4. Werte vs. Referenzen
  5. Was sind Events?
  6. Wie sind JavaScript und HTML verbunden?
  7. Praxis

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 mit await pausieren
  • Fehler können ganz normal mit try…catch behandelt werden
  • await kann nicht außerhalb einer async-Funktion verwendet werden
  • async 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. wrapper
  • event.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>-Element
  • document.body ist eine Referenz auf euer <body>-Element
  • document.head ist eine Referenz auf euer <head>-Element
  • document.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ück
  • document.querySelectorAll(cssSelector) wählt alle Elemente im Baum aus, auf die der übergebene CSS-Selektor zutrifft und gibt ein NodeList-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. Mit Array.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 ID id zurück, in manchen Fällen ist das schneller als document.querySelector('#id'), aber wichtig: in querySelector-Funktionen müsst ihr die ID wie in CSS mit # angeben, in getElementByIdohne#!

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 befinden
  • element.firstElementChild das erste Kind-Element
  • element.lastElementChild das letzte Kind-Element
  • element.parentElement das Eltern-Element des Elements, d.h. das Element, dass das referenzierte Element umgibt
  • element.nextElementSibling und element.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.

Spezialfälle: style und class

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 im class-Attribut im HTML steht, wenn ihr ihn ändert hat es den gleichen Effekt, wie wenn ihr ihn direkt im HTML ändern würdet

  • element.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 von node, wie Array.push(). Die Elemente sind Kinder von node, also innerhalb von node
  • node.prepend(element [, element]) fügt eines oder mehrere Elemente an den Anfang von node, wie Array.unshift(). Die Elemente sind Kinder von node, also innerhalb von node
  • node.before(element [, element]) fügt eines oder mehrere Elemente vornode ein, die neuen Elemente sind Geschwister von node, also außerhalb von node
  • node.after(element [, element]) fügt eines oder mehrere Elemente nachnode ein, die neuen Elemente sind Geschwister von node, also außerhalb von node
  • node.replaceWith(element [, element]) ersetzt node 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 deeptrue 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. 🎉