05: Einführung in die Programmierung

Um einem Computer sagen zu können, was er zu tun hat, muss man wissen, wie ein Computer „denkt“. Man muss nicht gut in Mathe sein, um programmieren zu können, man muss nur logisch denken und klare Anweisungen geben können.

Inhalt

  1. Was kann ein Computer?
  2. Was ist eine Programmiersprache?
  3. Was ist das einfachste Programm?
  4. Was sind die Grundkonzepte?
  5. Wie entwickelt man ein Programm?
  6. Wie geht man mit Fehlermeldungen um?
  7. Was ist eine Dokumentation und wie liest man die?
  8. Praxis

Was kann ein Computer?

Computer sind eigentlich ziemlich dumm, sie können vielleicht gut rechnen, wie der Name es schon sagt, aber außer dem können sie ohne Hilfe wirklich nicht viel. Unsere Aufgabe als Entwickler ist es, dem Computer zu erklären, wie er die Aufgaben lösen kann, die wir ihm aufgeben. Wir übersetzen also abstrakte Konzepte in klare Anweisungen, die der Rechner dann ausführen kann.

Wo liegen also die Stärken eines Computers:

  • Rechnen
  • Monotone Wiederholungen
  • Anweisungen zu 100% genau befolgen
  • Mit großen Datenmengen umgehen und den Überblick bewahren

Und seine Schwächen:

  • Kreativ denken
  • Denken überhaupt
  • Mit vagen Anweisungen klarkommen
  • Interpretieren
  • Usw.

Im Prinzip kann ein Computer also nur genau das Ausführen, was ihr ihm sagt. Wie genau das funktioniert liegt ein bisschen außerhalb dieses Kurses, aber für diejenigen unter euch, die es interessiert: ihr könnt einfach einmal in die Rechnerarchitektur- und Betriebssysteme-Vorlesungen an der LMU hineinschnuppern. Die bringen das ganze eigentlich auf den Punkt.

Für uns wichtig ist aber: wie sage ich dem Computer, was er machen soll? Und vor allem, wie er es machen soll, denn das weiß er auch nicht.

Was ist eine Programmiersprache?

Viele von euch werden bereits gehört haben, dass ein Computer „mit Einsen und Nullen“ funktioniert. Das steht sinnbildlich für die beiden Zustände, die ein Computer beherrscht: Strom an und Strom aus. Da es sich hierbei nur um zwei Zustände handelt, ist es ein binäres System. Man kann alle möglichen Daten binär ausdrücken, vor allem natürlich Zahlen, die für einen Rechner wichtig sind, aber auch Buchstaben und viel mehr. Während eines Informatikstudiums würdet ihr auch lernen, wie man Binärcode liest und schreibt, aber selbst ein Informatiker würde das nicht freiwillig machen, wenn es nicht absolut nötig wäre.

Deshalb wurden schon in den Anfangszeiten der Computer sogenannte Programmiersprachen entwickelt, die es Menschen erlauben, für sie verständlicheren Code zu schreiben, der dann für den Computer in Binärcode übersetzt wird. Diese „Übersetzung“ nennt man „Kompilierung“.

Kompiliert vs. Interpretiert

Je nachdem wann diese Kompilierung stattfindet, spricht man von einer kompilierten oder interpretierten Programmiersprache. Bei einer kompilierten Programmiersprache wie C oder C++ muss euer Code zunächst von einem Compiler komplett übersetzt werden, bevor ihr euer Programm ausführen könnt. Bei einer interpretierten Programmiersprache wie Python oder JavaScript hingegen wird euer Code Zeile für Zeile von einem sogenannten Interpreter übersetzt während das Programm schon läuft.

Deshalb könnt ihr zum Beispiel einfach node in eurem Terminal ausführen und so ein Programm schreiben, das sofort ausgeführt wird:

> let x = 2; // Variable x wird der Wert 2 zugewiesen
> let y = 5; // Variable y wird der Wert 4 zugewiesen
> x + y; // Variable x wird mit Variable y summiert
7 // das Ergebnis wird sofort angezeigt
> let z = x + y; // x und y sind weiterhin definiert, Variable z wird der Wert 7 zugewiesen

Tipp:

Code-Blöcke, die mit > beginnen, stehen fortan für Code, den ihr in Node ausführen könnt, d.h. nachdem ihr in einem Terminal node eingegeben habt. Beginnt eine Zeile in einem solchen Block nicht mit >, bedeutet das, dass es sich um die Ausgabe von Node handelt.

Das hat den enormen Vorteil, dass ihr schnell etwas ausprobieren könnt, z.B. was passiert wenn ihr 56 % 2 in JavaScript ausführt, ohne gleich ein ganzes Programm schreiben und kompilieren zu müssen. Eine kompilierte Sprache hat allerdings auf der anderen Seite natürlich Performance-Vorteile, denn ein Compiler kann während der Übersetzung den Code noch weiter optimieren.

Viele verschiedene Sprachen

Aber Amadeus, warum gibt es denn so viele verschiedene Programmiersprachen?

Programmiersprachen werden für die unterschiedlichsten Zwecke und Plattformen entwickelt. So hat zum Beispiel Apple mit Swift eine eigene Programmiersprache für die Entwicklung auf iOS-Systemen geschaffen, während Google Dart und Kotlin für Android unterstützt. Andere Programmiersprachen wie C++ versuchen bestehende Programmiersprachen, in diesem Fall C, zu verbessern und für den Programmierer angenehmer zu machen.

Je nach Programmiersprache gibt es zum Beispiel auch unterschiedliche Schlüsselwörter (z.B. let, if) und Befehle, sowie andere Regeln, zum Beispiel, wann man ein Semikolon benutzen muss und wann nicht. Diese Schlüsselwörter und Regeln nennt man die Syntax einer Programmiersprache, also ihre „Grammatik“.

Da am Ende allerdings immer für den Computer verständlicher Binärcode (oder Vergleichbares) dabei herauskommt, kann man theoretisch mit jeder Programmiersprache ein Programm für jede Plattform schreiben. (Praktisch ist das allerdings enorm umständlich und daher nicht zu empfehlen, weil man z.B. einen eigenen Compiler oder Interpreter schreiben müsste, die wiederum schon im Maschinencode vorliegen müssten, um das Programm zu übersetzen.)

Programmierparadigmen

Unterschiedliche Programmiersprachen bringen auch unterschiedliche Paradigmen mit sich, also verschiedene Programmier-Stile, wobei sich meistens mehrere Stile mit der selben Programmiersprache umsetzen lassen.

Zwei der größten Paradigmen, die sich in viele weitere Sub-Paradigmen unterteilen lassen, sind die folgenden:

Imperative Programmierung ist ein ein Stil, bei dem dem Computer direkte Befehle gegeben werden, die dieser dann nach und nach abarbeitet. Diese Anweisungen führen zu Ergebnissen, die dann mit weiteren Anweisungen weiterverarbeitet werden können. Wir werden hauptsächlich mit diesem Stil arbeiten.

Deklarative Programmierung ist das genaue Gegenteil von imperativer Programmierung. Hier beschreibt der Programmierer die Ergebnisse, die er / sie erhalten möchte und die Programmiersprache liefert diese Ergebnisse (meistens während der Interpretation) an den Programmierer zurück.

Typisierung

Ein weiteres Unterscheidungsmerkmal zwischen Programmiersprachen ist die sogenannte „Typisierung“. Vereinfacht gesagt legt diese fest, ob Variablen, die ihr deklariert einen festen Typ, z.B. eine Zeichenkette, oder eine Zahl haben, oder ob dieser Typ sich im Verlauf des Programms ändern kann.

JavaScript hat keine strikte Typisierung. Deshalb müsst ihr den Typ einer Variablen nicht festlegen, wenn ihr sie deklariert und ihr könnt der selben Variable einen Wert von einem anderen Typen zuweisen, so lang die Variable keine Konstante ist. Ich würde euch allerdings davon abraten, das zu tun, denn es kann besonders bei komplexeren Programmen schnell zu ungewollten Fehlern führen.

> let x; // Variable x wird definiert, aber hat keinen Wert
> typeof x; // Mit typeof könnt ihr den Typ einer Variable ausgeben lassen
'undefined' // x hat den Typ 'undefined'
> x = 1; // x wird der Wert 1 zugewiesen
> typeof x;
'number' // jetzt hat x den Typ 'number', ist also eine Zahl
> x = '1'; // x wird der Wert '1' zugewiesen
> typeof x;
'string' // x hat jetzt den Typ 'string', weil '1' keine Zahl, sondern eine Zeichenkette ist
> const y = 15; // y ist eine Konstante mit dem Wert 15
> typeof y;
'number'
> y = 'hallo'; // wirft einen Fehler, weil eine Konstante nicht verändert werden kann
Uncaught TypeError: Assignment to constant variable.

Was ist das einfachste Programm?

Das einfachste Programm der Welt ist wahrscheinlich die simple Addition zweier Zahlen: 1 + 1. Der Rechner führt diesen Befehl aus und gibt das Ergebnis zurück: 2. Trotzdem wird als Beispiel für das einfachste Programm aus Tradition das sogenannte Hello-World-Programm benutzt.

In JavaScript ist ein einfaches Hello-World nur eine Zeile lang:

> console.log('Hello World!'); // gibt 'Hello World' auf der Konsole aus

Programmierer sind allerdings faul, also warum mehr tippen als nötig? Schreiben wir unser Programm also lieber als eine einfache Funktion:

// hello.js
function sayHello() { // neue Funktion mit dem Namen 'sayHello'
  console.log('Hello World!');
}

// die Funktion muss aufgerufen werden, damit etwas passiert
sayHello();
sayHello();
sayHello();
sayHello();

Nun können wir unser Programm mit node hello.js ausführen und erhalten gleich vier „Hello World!“. 😉

Wir können es aber auch leichter erweitern:

// hello.js
function sayHello(name = 'World') {
  // name ist der Wert, der der Funktion beim Aufruf übergeben wird
  // wird nichts übergeben, ist der Wert von name 'World'
  console.log(`Hello ${name}!`); // Backticks (`) erlauben String-Templates, also Variablen in einer Zeichenkette
}

sayHello(); // Ausgabe: Hello World!
sayHello('Gustav'); // Ausgabe: Hello Gustav!
sayHello('Norbert'); // Ausgabe: Hello Norbert!

Und vielleicht ein letztes Mal:

// hello.js
function sayHello(name = 'World') {
  // if leitet eine Bedingung ein
  if (name === 'Amadeus')  { // wenn name genau gleich 'Amadeus'
    console.log('Hello Teacher!'); // dann führe diese Zeile aus
  } else { // wenn nicht
    console.log(`Hello ${name}!`); // führe diese Zeile aus
  }
}

sayHello(); // Ausgabe: Hello World!
sayHello('Gustav'); // Ausgabe: Hello Gustav!
sayHello('Norbert'); // Ausgabe: Hello Norbert!
sayHello('Amadeus'); // Ausgabe: Hello Teacher!

Habt ihr hello.js geschrieben, ausgeführt und verstanden, dann herzlichen Glückwunsch: ihr seid jetzt ein Programmierer. 😉 Mehr ist wirklich nicht dabei.

Was sind die Grundkonzepte?

Es gibt bestimmte Grundkonzepte, die in nahezu jeder (imperativen) Sprache vorhanden sind. Auf die werde ich nun am konkreten Beispiel JavaScript näher eingehen. Einige dieser Konzepte haben wir bereits in den vorhergehenden Beispielen gesehen, andere sind etwas komplexer, aber lasst uns nicht vergessen: der Computer ist dumm, er braucht klare und logische Anweisungen, deshalb haben die nachfolgenden Konzepte auch immer einen logischen Sinn.

Befehle / Anweisungen

Wie wir in unserem Hello-World Beispiel gesehen haben, geben wir bei der imperativen Programmierung unserem Computer konkrete Befehle. Diese Befehle nennt man auch „Statements“ und während JavaScript es durchaus erlaubt, mehrere Statements in eine Zeile zu schreiben und nur durch Semikolons (;) zu trennen, ist es für sauberen Code sinnvoll in den meisten Fällen nur eine Anweisung pro Zeile zu schreiben.

Beispiel:

let x = 1;
let y = 2;
let z = x + y;
console.log(x, y, z); // Ausgabe: 1 2 3
// Auch möglich, aber weniger lesbar:
let u = 'foo'; let v = 'bar'; console.log(u + v); // Ausgabe: foobar

Anweisungen sind zum Beispiel Variabelzuweisungen und Funktionsaufrufe.

Kommentare

Damit ihr euren Code später noch einmal versteht und vor allem auch Andere, die euren Code lesen verstehen, was ihr meint, solltet ihr Kommentare hinterlassen.

In JavaScript gibt es zwei Formen von Kommentaren: einzeilige Kommentare, die mit // eingeleitet werden, und mehrzeilige Kommentare, die mit /* */ umgeben werden:

// dies ist ein einzeiliger Kommentar
let x = 1; // er kann auch nach einem Statement stehen

/* Dies ist ein mehrzeiliger Kommentar
Er unterstützt Zeilenumbrüche. */

Alles, was innerhalb eines Kommentars steht, wird vom Computer komplett ignoriert. Hier noch ein paar Richtlinien, wann Kommentare Sinn machen:

  • Wenn der Code nicht selbsterklärend ist
  • Wenn ihr einen Hack oder einen Workaround verwenden müsst, dann aber auch mit Begründung, weshalb
  • Wenn ihr eine komplexe Funktion definiert, um Anderen klarer zu machen, was genau passiert

Wenn euer Code sauber ist, d.h. korrekte Einrückung verwendet, sinnvolle Variablennamen benutzt, und sich nicht unnötig wiederholt, werdet ihr nicht viele Kommentare brauchen, denn er sollte dann selbsterklärend sein. Seht es so: wenn ihr Kommentare benötigt, um den Code zu verstehen, könnt ihr ihn wahrscheinlich so verbessern, dass ihr die Kommentare nicht mehr braucht.

Da viele von euch allerdings noch Anfänger sind, die noch nicht so viel Code gelesen haben, werde ich natürlich weiterhin viele Kommentare in den Codebeispielen hinterlassen und ihr könnt euch natürlich auch Notizen an euren Code schreiben, damit ihr später noch nachvollziehen könnt, was er macht.

Variablen

In mehreren Beispielen ist nun schon das Wort „Variable“ gefallen. Viele von euch werden sich wohl noch an den Mathematikunterricht in der Oberstufe erinnern, in dem ihr auch schon mit Variablen in Berührung gekommen seid. Grob beschrieben ist eine Variable einfach eine Box, in die ihr einen Wert stecken könnt.

In JavaScript kann dieser Wert alles Mögliche sein, eine Zahl, eine Zeichenkette, sogar eine ganze Funktion. Ihr werdet in eurem Code Variablen benutzen, um Informationen für die Dauer der Ausführung des Programms zu speichern.

Wie man Variablen deklariert, haben wir in vielen Beispielen schon gesehen:

> let x; // die Variable x wird deklariert

Ihr könnt einer Variable natürlich auch schon während der Deklaration einen Wert zuweisen:

> let x = 1; // die Variable x wird deklariert und erhält den Wert 1

Habt ihr eine Variable deklariert und ihr einen Wert zugewiesen, könnt ihr diesen Wert natürlich zu einem späteren Zeitpunkt wieder ändern, indem ihr ihr einfach einen neuen Wert zuweist, passt dabei aber auf, dass ihr nicht versucht die Variable neu zu deklarieren, denn das führt zu einem Fehler:

> let x = 1; // die Variable x wird deklariert und erhält den Wert 1
> console.log(x);
1
> x = 2; // der Variable x wird der Wert 2 zugewiesen
> console.log(x);
2
> let x = 3 // funktioniert nicht
Uncaught SyntaxError: Identifier 'x' has already been declared

Achtung:

JavaScript erlaubt es euch auch, Variablen zu deklarieren, indem ihr einfach variableName = wert; schreibt (also ohne let). Das ist ein Überbleibsel aus der Vergangenheit und sollte nicht verwendet werden! Das gleiche gilt für die veraltete Variabledeklaration mit var.

Den Namen, den ihr in JavaScript einer Variable gebt, könnt ihr mit ein paar Einschränkungen frei bestimmen:

  • Der Name darf nur Zahlen, Buchstaben und die Symbole $ und _ enthalten
  • Der Name darf nicht mit einer Zahl beginnen
  • Der Name darf kein reserviertes Schlüsselwort (z.B. if, let, else, usw.) sein
  • Der Name darf zwar internationale Zeichen wie Umlaute (äöü) etc. enthalten, aber es wird davon abgeraten
  • Da die Syntax von JavaScript an Englisch angelegt ist, macht es auch Sinn euren Variablennamen englische Namen zu geben

Ob ihr Klein- oder Großbuchstaben verwendet macht einen Unterschied:

> let amadeus = 'Teacher';
> console.log(amadeus);
'Teacher'
> console.log(Amadeus); // Fehler weil A statt a
Uncaught ReferenceError: Amadeus is not defined

Da Variablen keine Leerzeichen enthalten dürfen, werden Variablennamen, die aus mehreren Wörtern bestehen meistens im sogenannten „camelCase“ geschrieben, also z. B.:

> let meineHausaufgabe = 'hat mein Hund gefressen';

Außerdem sollte der Name zu Verstehen geben, was in der Variable gespeichert wird:

> let xyz = 'Amadeus'; // dem Computer ist egal, welchen Namen die Variable hat
> let name = 'Amadeus'; // aber für uns als Entwickler macht es Sinn, verständliche Namen zu geben
> let teacherName = 'Amadeus'; // noch besser, weil noch spezifischer

Zusammenfassung: ihr könnt eine Variable mit let gefolgt von einem Namen deklarieren und ihr mit = einen beliebigen Wert zuweisen. Diesen Wert könnt ihr Verändern, wenn ihr der Variable einen neuen Wert mit = zuweist.

Konstanten

Konstanten sind spezielle Variablen, deren Wert nach der Deklaration nicht mehr verändert werden darf. Sie werden statt mit let mit const deklariert und müssen zwingend bei der Deklaration einen Wert übergeben bekommen. Ansonsten gelten für sie die gleichen Regeln wie für normale Variablen:

> const NAME = 'Amadeus'; // Konstante NAME wird deklariert und bekommt den Wert 'Amadeus'
> console.log(NAME);
'Amadeus'
> NAME = 'Günther'; // Fehler, weil Konstante
Uncaught TypeError: Assignment to constant variable.

Aus Konvention heraus werden die Namen von Konstanten, die vor der Ausführung des Codes bekannt sind immer in Versalien geschrieben, also NAME statt name. Falls der Name einer solchen Konstante aus mehreren Wörtern besteht, schreibt man ihn mit dem sogenannten „SCREAMING_SNAKE_CASE“. Konstanten die erst bei der Ausführung des Programms bekannt werden, z.B. durch Berechnung, werden allerdings wie normale Variablen geschrieben:

> const PI = 3.14159; // vorher bekannt, daher Versal
> const MEINE_HAUSAUFGABE = 'hat mein Hund gefressen'; // leider auch vorher bekannt
> const vierMalPi = 4 * PI; // vor Ausführung unbekannt, bzw. zur Laufzeit berechnet, daher in camelCase

Wann immer ihr wisst, dass ihr einer Variablen keinen neuen Wert zuweisen werdet, solltet ihr diese als eine Konstante definieren – das ist in den meisten Fällen der Fall.

Geltungsbereich von Variablen und Konstanten

Variablen und Konstanten sind in JavaScript nur innerhalb ihrer jeweiligen „Scope“, also ihrem Geltungsbereich deklariert. Eine neue Scope entsteht immer dann, wenn Code sich zwischen zwei geschweiften Klammern ({…}) befindet (einem neuen Block). Die „oberste“ Scope ist immer die Datei, in der der Code ausgeführt wird. Diese wird auch „globale“ Scope genannt.

Das bedeutet im Umkehrschluss, dass es wichtig ist, wo ihr in eurem Code Variablen definiert:

// variablesTest.js
const TEACHER_NAME = 'Amadeus'; // TEACHER_NAME wird in globalen Scope deklariert

function greet(student) {
  const studentName = student.name; // studentName wird in der Scope von 'greet' deklariert

  if (studentName === TEACHER_NAME) {
    const ERROR_MESSAGE = 'Person cannot greet itself'; // ERROR_MESSAGE wird in der Scope des 'if' deklariert
    console.log(ERROR_MESSAGE);
  } else {
    console.log(`${TEACHER_NAME} sagt: Hi, ${studentName}!`);
  }
}

greet({ name: 'Günther', semester: 4 }); // Ausgabe: 'Amadeus sagt: Hi, Günther!'
console.log(studentName); // Fehler: studentName ist nicht außerhalb von 'greet' deklariert

Wird das obige Beispiel ausgeführt, erhalten wir am Ende eine Fehlermeldung. Das passiert, weil wir versuchen, den Wert der Variablen auszugeben, die wir innerhalb der Funktion deklariert haben. Trotzdem können wir aber innerhalb der Funktion auf die Variable TEACHER_NAME zugreifen. Warum?

In JavaScript können wir stets auf die Variablen zugreifen, die in einer „höheren“ Scope deklariert werden, aber nicht auf diejenigen, die in einer „tieferen“ Scope deklariert werden.

Stellt euch vor, Scopes sind Räume mit einem falschen Spiegel. Von Innen kann man nach Draußen sehen, aber von Draußen nicht nach Innen. Wenn wir jetzt eine Schachtel (Variable) innerhalb des Raumes haben, können wir von außen nicht sehen, was sich in ihr befindet. Wir können aber von innen sehen, was sich in einer Schachtel außerhalb des Raumes befindet.

Aber Amadeus, dann macht es doch Sinn einfach alle Variablen in der globalen Scope zu definieren, oder?

Nein, das ist eine sehr schlechte Idee. Variablen sollten immer nur in der Scope definiert werden, in der sie auch verwendet werden. Somit kann es nicht passieren, dass ihr aus Versehen in zwei völlig verschiedenen Teilen eurer Anwendung den selben Variablennamen verwendet und es dann zu sehr schwer identifizierenden Fehlern kommt:

// usefulFunctions.js
function add(a, b) { // Funktionsargumente werden nur für die Scope der Funktion deklariert
  return a + b;
}

function multiply(a, b) { // deshalb können wir hier die selben Namen verwenden
  return a * b;
}

function addAndMultiply(a, b, c) {
  const result = add(a, b);
  return multiply(result, c);
}

function multiplyAndAdd(a, b, c) {
   /* wir können result hier problemlos wieder deklarieren, weil das result aus
      addAndMultiply in einer tieferen Scope sitzt und somit für unsere Funktion
      nicht existiert */
  const result = multiply(a, b);
  return add(result, c);
}

Man hätte im obigen Beispiel natürlich result global mit let deklarieren können und dann innerhalb der Funktionen den Wert mit einer einfachen Zuweisung ändern können, aber es wäre nicht logisch das zu tun, denn result wird ja nur innerhalb der Funktionen gebraucht. In diesem einfachen Beispiel würden dadurch zwar noch keine Fehler generiert, aber in einer komplexeren Anwendung könnte es durchaus passieren, dass eine andere Funktion etwas mit dem globalen result macht, oder einen bestimmten Wert in dieser Variable erwartet, was zu unerwarteten Fehlern führt.

Außerdem entfernt die JavaScript-Laufzeitumgebung automatisch Variablen, die nicht mehr gebraucht werden aus dem Arbeitsspeicher eures Computers, aber eine globale Variable muss so lange im Speicher behalten werden, bis das Programm beendet ist, was natürlich auf lange Sicht dazu führen kann, dass die Performance eurer Anwendung abnimmt.

Zusammenfassung: in JavaScript sind Variablen nur innerhalb der Scope sichtbar, in der sie deklariert wurden. Tiefere Scopes können die Variablen von höheren Scopes sehen, aber nicht von tieferen Scopes. Variablen sollten immer nur in der Scope definiert werden, in der sie verwendet werden.

Typen

Wie weiter oben schon angemerkt, ist JavaScript eine dynamisch typisierte Programmiersprache. Dennoch gibt es auch in JavaScript acht grundlegende Typen. Jede Variable, die ihr deklariert, wird einem dieser Typen angehören. Ausgeben könnt ihr euch den Typ mit Hilfe von typeof.

Jeder dieser Typen hat spezielle Funktionen, die es vereinfachen, mit ihm zu arbeiten.

Hier sind die acht „basic types“ von JavaScript:

  1. Number: Number beinhaltet Zahlen wie Integer (ganze Zahlen) und Floats (Gleitkommazahlen). Auch die besonderen Werte Infinity, -Infinity, und NaN sind vom Typ „Number“.

    > typeof 1;
    'number'
    > typeof 1.2;
    'number'
    > 1 / 0
    Infinity
    > const x = 1 / 0;
    > typeof x;
    'number'
    > 'keine Zahl' / 2; // das Ergebnis kann keine Zahl sein, also wird es NaN (Not a Number)
    NaN
    > '2' / 2; // SONDERFALL: JavaScript versucht Zeichenketten in Zahlen zu konvertieren und führt die Rechnung dann aus
    1

    Der Number-Typ bringt außerdem einige nützliche Funktionen mit sich:

    • Number(wert) konvertiert Wert in eine Zahl, z.B. Number('12') === 12

    • Number.isNaN(zahl) gibt zurück, ob die übergebene Zahl NaN ist

    • Number.isFinite(zahl) gibt zurück, ob die übergebene Zahl endlich ist

    • Number.isInteger(zahl) gibt zurück, ob die übergebene Zahl eine ganze Zahl ist

    • Number.parseFloat(wert) und Number.parseInt(wert, radix) konvertieren einen Wert in eine Gleitkomma-, bzw. ganze Zahl. Radix bestimmt hier das Zahlensystem des Eingabewerts(2 für Binär, 10 für Dezimal, 16 für Hexadezimal, usw.):

      > Number.parseInt('15xyz', 10) // funktioniert nur, weil 10 am Anfang der Zeichenkette steht
      15
      > Number.parseInt('xyz15', 10)
      NaN
      > Number.parseInt('0100', 2) // so schreibt man 4 in Binärcode
      4
      > Number.parseInt('f', 16) // so schreibt man 15 in Hexadezimal
      15

      Mehr Informationen auf DevDocs.

  2. BigInt: der Number-Typ in JavaScript kann technisch bedingt nur Zahlen bis 2⁵³, bzw. 2⁻⁵³ abbilden, wird die Zahl größer oder kleiner als dieser Wert, springt sie zurück auf 0 und verursacht so einen sogenannten Integer- Overflow. In den meisten Fällen erreicht man nie so hohe Zahlen, aber für manche Verwendungszwecke wie Kryptografie kann es vorkommen. Deshalb gibt es in Node, Chrome und Firefox den Typ BigInt. Ich führe ihn hier der Vollständigkeit halber auf, wir werden diesen Typ im Verlauf des Kurses nicht benötigen (und die meisten von uns wahrscheinlich niemals). Für besonders Neugierige unter euch gibt es hier mehr Informationen dazu.

  3. String: Zeichenketten, also beliebige Zeichen in einer Reihe von einem oder mehr, werden „Strings“ genannt. In JavaScript müssen diese Zeichenketten mit Anführungszeichen umgeben werden, um von Schlüsselwörtern und Variabelnamen klar abgegrenzt werden zu können. Es ist prinzipiell egal, ob ihr doppelte (""), oder einfache ('') Anführungszeichen verwendet. Im Airbnb- Code-Style werden allerdings immer einfache Anführungszeichen verwendet, außer ihr wollt einfache Anführungszeichen innerhalb des Strings verwenden:

    > const aString = 'Ein ganz normaler String';
    > const bString = "Ein ganz normaler String"; // doppelte Anführungszeichen gehen auch, sollten aber vermieden werden
    > aString === bString; // es ist egal, welche Anführungszeichen man benutzt, das Ergebnis ist identisch
    true
    > const cString = "Doppelte Anführungszeichen sollten benutzt werden, wenn man im 'String' Einfache benutzt.";
    > const dString = 'Man kann die Anführungszeichen aber auch \'escapen\', dann kann man auch Einfache benutzen';

    Wie ihr in der letzten Zeile des Beispiels sehen könnt, könnt ihr Zeichen auch mit einem vorangestellten Backslash () „escapen“. Das bedeutet, dass die Laufzeitumgebung das Zeichen als das Zeichen selbst und nicht ein Steuerzeichen interpretiert. In unserem Beispiel würde das einfache Anführungszeichen vor „escapen“ eigentlich den String beenden, da aber ein Backslash vorangestellt wurde, weiß die Laufzeitumgebung, dass es einfach nur das Zeichen sein und nicht den String beenden soll. Das bedeutet allerdings auch, dass ihr einen Backslash escapen müsst, wenn ihr ihn in einem String verwenden wollt:

    > const unescaped = 'Hier kommt ein Backslash (\)'; // der Backslash hat hier die schließende Klammer escaped und wird dann ignoriert
    > const escaped = 'Hier kommt ein Backslash (\\)'; // hier sagen wir der Laufzeitumgebung, dass der Backslash einfach nur ein Backslash ist
    > unescaped === escaped; // die Strings sind nicht gleich
    false
    > console.log(unescaped); // wenn wir sie ausgeben, wird klar, warum
    'Hier kommt ein Backslash ()'
    > console.log(escaped);
    'Hier kommt ein Backslash (\)'

    Ein besonderes Zeichen, dass euch in diesem Kontext öfter begegnen wird ist \n. Nach dem gerade gelernten sollte das eigentlich für JavaScript wie ein einfaches „n“ aussehen – allerdings ist \n ein Sonderzeichen für sich selbst und signalisiert einen Zeilenumbruch. Es gibt mehrere solcher Sonderzeichen in Strings, falls ihr also einmal nicht das Sonderzeichen, sondern zum Beispiel die Zeichen „\“ und „n“ meint, müsst ihr sie escapen:

    > const unescaped = 'Das ist ein\nZeilenumbruch';
    > const escaped = 'Das ist ein\\nZeilenumbruch';
    > console.log(unescaped);
    Das ist ein
    Zeilenumbruch
    > console.log(escaped);
    Das ist ein\nZeilenumbruch
    > const tab = 'Ein Tabulator wird mit \\t dargestellt: \t tabbed'; // wie gesagt, es gibt noch mehr dieser Zeichen
    > console.log(tab);
    Ein Tabulator wird mit \t dargestellt:    tabbed

    Es wird oft vorkommen, dass ihr die Werte von Variablen innerhalb eines Strings ausgeben möchtet. Natürlich könnt ihr Strings durch Addition aneinander ketten, aber in modernem JavaScript gibt es sogenannte „Template- Literals“, die euch diese Aufgabe vereinfachen. Diese besonderen Strings werden mit Backticks umgeben (`) und können dann innerhalb der besonderen Zeichenfolge ${} normales JavaScript ausführen:

    > const likes = 'Wein und Käse';
    > const aString = 'Ich mag ' + likes + '.';
    > console.log(aString);
    Ich mag Wein und Käse.
    > const bString = `Ich mag ${likes}.`;
    > aString === bString;
    true
    > console.log(bString);
    Ich mag Wein und Käse.
    > console.log(`Man kann hier sogar Rechnen: ${ 5 + 10 * 3 }!`);
    Man kann hier sogar Rechnen: 35! // JavaScript wird ausgeführt

    Wie auch der Number-Typ, bringt der String-Typ viele nützliche Funktionen mit sich, unter Anderem:

    • 'ein String'.charAt(index) gibt das Zeichen am angegebenen Index zurück, kann auch als 'ein String'[index] geschrieben werden
    • 'ein String'.includes(wert) gibt zurück, ob wert im String vorhanden ist
    • 'ein String'.toUpperCase() und 'ein String'.toLowerCase() konvertieren den String in Groß-, bzw. Kleinbuchstaben
    • 'ein String'.trim() entfernt „Whitespace“, d.h. Zeichen, die nicht sichtbar sind wie Leerzeichen und Newline-Zeichen vom Anfang und Ende des Strings
    • 'ein String'.replace(wert, ersatz) ersetzt wert im String mit ersatz, z.B. 'ein String'.replace('ein', 'ein anderer') würde 'ein anderer String' ergeben
  4. Boolean: Booleans sind die Wahrheitswerte true und false. Diese werden vor allem dazu verwendet, um die Konzepte „ja“ und „nein“ auszudrücken:

    > 3 > 5 // ist 3 größer als 5?
    false // nein
    > 10 <= 20 // ist 10 kleiner gleich 20?
    true // ja
    > let ausgeruht = false;
    > ausgeruht = powernap(); // die Variable ausgeruht erhält den Rückgabewert der ausgedachten powernap-Funktion, in diesem Fall true
    > console.log(ausgeruht);
    true
    > typeof ausgeruht
    'boolean'

    Mit Hilfe der Funktion Boolean(wert) könnt ihr jeden Wert in einen Boolean konvertieren. Beachtet dabei: '', 0, null, undefined werden zu false, aber 'false', 1 und so ziemlich jeder andere Wert werden zu true. Dieser Umstand wird später in Bedingungen wichtig und kann zu Verwirrung führen.

  5. null: in JavaScript bedeutet der null-Typ einfach nur, dass etwas nicht existiert, oder unbekannt ist. Eine Besonderheit ist, dass typeof null'object' zurückgibt. Ihr könnt also nur überprüfen, ob eine Variable den Wert null enthält, wenn ihr sie mit dem strikten Vergleichsoperator === mit null vergleicht:

    > const nichts = null;
    > typeof nichts;
    'object'
    > nichts === null;
    true

    Eine Variable mit dem Wert null wird nicht automatisch aus dem Speicher entfernt! Sie existiert einfach weiter mit dem Wert null, es macht also keinen Sinn, unbenötigte Variablen auf null zu setzen.

  6. undefined: ähnlich wie null steht undefined in JavaScript für „deklariert, aber noch kein Wert zugewiesen“. Theoretisch kann undefined einer Variable als neuer Wert zugewiesen werden, aber in diesen Fällen ist es besser null zu verwenden. Eine Überprüfung von typeof variableName auf den Wert 'undefined' macht Sinn, um sicherzugehen, dass eine Variable einen Wert hat, bevor sie weiter verwendet wird:

    // undefinedTest.js
    function foo(bar) {
      if (typeof bar === 'undefined') {
        bar = 'not passed in'; // wenn bar undefiniert ist, weisen wir einen Wert zu
      }
      return `The argument was ${bar}.`;
    }
    
    console.log(foo()); // gibt 'The argument was not passed in.' aus
    console.log(foo('baz')); // gibt 'The argument was baz.' aus
  7. Object: im Gegensatz zu den anderen Typen, die jeweils nur einen einzigen Wert enthalten können, sind Objekte in der Lage komplexe Datenstrukturen zu beinhalten. Datenstrukturen wie Listen, die von Außen wie ein eigener Typ wirken und es in anderen Programmiersprachen auch oft sind, sind in JavaScript einfach besondere Objekte. Objekte sind hochkomplex und bieten Stoff für mehrere Vorlesungen, aber für unsere Zwecke reicht es aus, wenn ihr sie vorerst als „Sammlungen“ von Variablen seht, wie ein Wörterbuch aus Schlüssel und Wert-Paaren. Diese einfachen Objekte definiert ihr, indem ihr einer Variablen eine Reihe von Schlüssel/Wert Paaren innerhalb von geschweiften Klammern ({}) übergebt:

    > const person = { name: 'Gustav', lieblingsfarbe: 'rot', alter: 10, angemeldet: true, pronomen: 'er' };
    > console.log(person.name); // Zugriff auf Wert über Schlüssel, z.B. 'name'
    Gustav
    > console.log(`Das ist ${person.name}, ${person.pronomen} ist ${person.alter} Jahre alt.`);
    Das ist Gustav, er ist 10 Jahre alt.
    > const farbFeld = 'lieblingsfarbe';
    > console.log(person[farbFeld]); // man kann auch Schlüssel in Variablen stecken, muss dann aber mit dieser Syntax arbeiten
    rot
    > person.age = 11; // Werte können geändert werden
    > person['favourite food'] = 'Nudelsalat'; // Schlüssel können auch Strings sein und im Nachhinein hinzugefügt werden
    > delete person.name; // Schlüssel/Wert Paare können mit delete aus einem Objekt gelöscht werden.
    > console.log(person);
    {
      lieblingsfarbe: 'rot',
      alter: 10,
      angemeldet: true,
      pronomen: 'er',
      age: 11,
      'favourite food': 'Nudelsalat'
    }

    Für unsere Zwecke besonders nützlich sind außerdem folgende Funktionen des Object-Typs:

    • Object.entries(objekt) gibt uns eine Liste aller Schlüssel/Wert Paare von objekt in der Form [Schlüssel, Wert] zurück.
    • Object.keys(objekt) gibt uns eine Liste aller Schlüssel von objekt
    • Object.values(objekt) gibt uns eine Liste aller Werte von objekt

    Für eine detailliertere Beschreibung von Objekten, könnt ihr euch diesen Artikel durchlesen – aber ich bin der Meinung, dass ihr euch leichter tun werdet, wenn ihr Objekte organisch über die Praxis kennenlernt.

  8. Symbol: Symbole werden dafür verwendet, Objektinstanzen einzigartige Identifier zuzuweisen und sind nur der Vollständigkeit halber gelistet.

Bonus: Arrays sind besondere Objekte, mit denen ihr so oft zu tun haben werdet, dass man fast meinen könnte es wäre ein grundlegender Typ wie String oder Number. Grob gesagt sind Arrays geordnete Listen – hierbei unterscheiden sie sich wesentlich von Objekten, die generell ungeordnet sind. Auch sind die „Schlüssel“ von Arrays immer Zahlen, der sogenannte „Index“, also die Position des Elements in der Liste.

Ein neues Array definiert ihr indem ihr einer Variablen eine Liste von Werten innerhalb von eckigen Klammern ([]) zuweist:

> const myArray = ['wert', 1, { key: 'value' }, true];
> console.log(myArray[2]);
{ key: 'value' }

Ein Array kann gleichzeitig Werte von allen möglichen Typen beinhalten, auch weitere Arrays und Objekte. Zugegriffen wird wie bei Objekten mit der Klammer- Syntax: array[index], wobei index immer eine Zahl zwischen 0 und der Länge des Arrays minus 1 sein muss.

Achtung:

Wie ihr seht ist der erste Index immer 0, was bedeutet, dass das erste Element im Array den Index 0 hat! Das ist in fast allen Programmiersprachen so und wird euch mit der Zeit als völlig selbstverständlich erscheinen. Eine Begründung dafür findet ihr hier, falls ihr es unbedingt wissen wollt. Ansonsten akzeptiert einfach, dass man beim Programmieren sogut wie immer von 0 aufwärts zählt.

Das erste Element im Array ist also nameDesArrays[0]. Die Länge eines Arrays könnt ihr über nameDesArrays.length ausgeben lassen. Um auf das letzte Element in einem Array zuzugreifen, ohne eine feste Länge definieren zu müssen, könnt ihr also einfach nameDesArrays[array.length - 1] schreiben.

Wie auch schon bei Objekten könnt ihr natürlich den Wert an einem Index beliebig austauschen:

> const myArray = ['one', 'two', 'three'];
> myArray[1] = 'two and a half';
> console.log(myArray);
['one', 'two and a half', 'three']

Theoretisch könnt ihr so auch neue Elemente zum Array hinzufügen, aber es macht mehr Sinn die extra dafür verfügbaren Funktionen zu verwenden:

> const myArray = ['one', 'two', 'three'];
> myArray.push('four'); // fügt den Wert am Ende des Arrays ein
> myArray.unshift('zero'); // fügt den Wert am Anfang des Arrays ein
> console.log(myArray);
['zero', 'one', 'two', 'three', 'four']
> myArray.pop() // entfernt den letzten Wert des Arrays
> console.log(myArray);
['zero', 'one', 'two', 'three']
> myArray.shift() // entfernt den ersten Wert des Arrays
> console.log(myArray);
['one', 'two', 'three']
> myArray.splice(2, 0, 'two and a half'); // fügt einen Wert am angegebenen Index ein
> console.log(myArray);
[ 'one', 'two', 'two and a half', 'three' ]
> myArray.splice(2, 1); // löscht den Wert am angegebenen Index
> console.log(myArray);
[ 'one', 'two', 'three' ]

Wichtig dabei ist, dass es deutlich schneller ist, push() und pop() zu verwenden, als shift() und unshift(). splice() ist eine besonders komplexe Funktion, die ihr nur dann verwenden solltet, wenn es nicht anders möglich ist. Was genau sie alles kann, findet ihr hier.

Andere nützliche Funktionen für Arrays sind:

  • nameDesArrays.forEach() um eine Funktion für jeden Wert des Arrays aufzurufen
  • nameDesArrays.sort() zum Sortieren des Arrays
  • Array.isArray(nameDesArrays) zum überprüfen, ob nameDesArrays auch wirklich ein Array ist (da typeof nameDesArrays === 'object')
  • nameDesArrays.indexOf(wert) um den Index von wert zu finden
  • nameDesArrays.join(zeichen) um das Array zu einem String mit dem Trennzeichen zeichen zu konvertieren

Zusammenfassung: es gibt 8 basis-Typen in JavaScript, aber wir werden meistens nur mit String, Number, Boolean, null, und Object arbeiten. Arrays sind spezielle Objekte, die sich hervorragend dafür eignen, sortierte Listen zu repräsentieren. Alle Typen und Arrays haben spezielle Funktionen, die uns das Leben erleichtern.

Funktionen

Mehrfach ist nun schon der Begriff Funktion gefallen, den manche von euch vielleicht auch als Methode kennen. Wie Variablen seid ihr damit auch schon im Mathematikunterricht in Berührung gekommen, z.B. in der Form eines Polynoms f(x) = x² – Funktionen in JavaScript funktionieren ähnlich, ihr schreibt eine Reihe von Anweisungen, die einen Input verarbeiten und dadurch einen Output generieren. Im Endeffekt also ein Programm im Programm und tatsächlich sind Funktionen auch die Bausteine eurer Programme.

Ihr größter Nutzen ist es, euren Quellcode zu vereinfachen und Wiederholungen zu vermeiden. Wir werden später einen Algorithmus zum mischen eines Kartenstapels entwickeln und ihr könntet diesen Algorithmus natürlich jedes mal neu abtippen, wenn ihr einen Kartenstapel mischen müsst, aber es ist viel einfacher und weniger Fehleranfällig, diesen Algorithmus in eine Funktion zu stecken, die man dann einfach immer wieder aufrufen kann.

Es gibt mehrere Möglichkeiten in JavaScript eine Funktion zu definieren, aber wir werden uns nur mit den beiden häufigsten Formen auseinander setzen: der „function declaration“ und der „Arrow-Function“, aber ungleich der Definitionsform sind alle Funktionen gleich aufgebaut:

// functionTest.js

// einfache Function Declaration
function sayHello() {
  console.log('Hello World!');
}

// das selbe als Arrow-Function, nützlich für Einzeiler
const sayHelloArrow = () => console.log('Hello World!');

// einmal definiert können sie beliebig oft aufgerufen werden
sayHello();
sayHello();
sayHelloArrow();
sayHelloArrow();

Um den Output einer Funktion zu kontrollieren kann man ihr einen Input übergeben. Diesen Input, der jegliche Form von Daten sein kann, nennt man „Argumente“ oder „Parameter“:

// greet.js

// Function Declaration mit Argumenten
// name und vorname sind Variablen und verhalten sich als wären sie mit 'let' deklariert
function greet(name, vorname) {
  console.log(`Hello ${vorname} ${name}!`);
}

// Als Arrow-Function
const greetArrow = (name, vorname) => console.log(`Hello ${vorname} ${name}!`);

// beim Aufrufen der Funktionen können dann Werte übergeben werden
greet(); // name und vorname sind undefiniert, also Ausgabe: 'Hello undefined undefined!'
greet('Amadeus'); // name ist 'Amadeus', vorname ist undefiniert, also Ausgabe: 'Hello undefined Amadeus!'
greetArrow('Günther', 'Jauch'); // name ist 'Günther', vorname ist 'Jauch', also Ausgabe: 'Hello Jauch Günther!'
greetArrow('Gans', 'Gustav'); // name ist 'Gans', vorname ist 'Gustav', also Ausgabe: 'Hello Gustav Gans!'
greet(2, 1); // Argumente können jeder mögliche Typ sein, Ausgabe: 'Hello 1 2!'

Da Funktionsargumente sich verhalten als wären sie mit let innerhalb der Funktion deklariert sind sie immer vom Typ undefined wenn beim Aufrufen der Funktion kein Argument übergeben wird. Um das zu verhindern kann man ihnen einen „Default“-Wert geben:

// greetWithDefault.js

// Arrow-Funktionen können auch Mehrzeilig sein
const greet = (name = 'Unbekannt', vorname = 'Anonymous') => {
  console.log(`Hello ${vorname} ${name}!`);
}

greet(); // keine Argumente übergeben, default wird benutzt, Ausgabe: 'Hello Anonymous Unbekannt!'
/* um für das erste Argument den default zu nutzen, muss es als undefined
übergeben werden, wenn man ein weiteres Argument angeben will */
greet(undefined, 'Amadeus'); // Ausgabe: 'Hello Amadeus Unbekannt!'

Standardmäßig geben Funktionen undefined zurück. Für das Beispiel oben:

// greetWithDefault.js
// …Rest der Datei…
const greetResult = greet();
console.log(typeof greetResult); // Ausgabe: 'undefined'

Um einen anderen Wert zurückzugeben, wird das return-Schlüsselwort verwendet:

// add.js

function add(a, b) {
  return a + b; // gib die Summe von a und b zurück
}

const result = add(1, 1);
console.log(result); // Ausgabe: 2
console.log(add(5, 5)); // Ausgabe: 10

/* Besonderheit bei einzeiligen Arrow-Functions: das Ergebnis des Statements
direkt nach dem Arrow (=>) wird zurückgegeben */
let multiply = (a, b) => a * b; // wie als stünde vor a * b 'return': => return a * b
console.log(multiply(2, 2)); // Ausgabe: 4

function returnTest(a, b) {
  return;
  let result = a + b; // wird nie ausgeführt, denn return beendet die Funktion
}

Achtung:

Falls sich innerhalb der Funktion Code nach dem return befindet, wird dieser ignoriert, denn die Funktion endet immer beim return. Das bedeutet auch, dass man die Ausführung einer Funktion mit return frühzeitig beenden kann, z.B. wenn ein Argument nicht vom richtigen Typ ist.

Wie Variablen könnt ihr Funktionen frei benennen. Allerdings gelten auch hier die selben Einschränkungen und Empfehlungen wie für Variabelnamen. Außerdem solltet ihr immer das Prinzip der Single Responsibility im Kopf behalten, also immer nur eine Aktion pro Funktion ausführen. Es macht keinen Sinn, wenn die Funktion login(username, password) auch die Funktion ist, mit der ein Account erstellt wird, dafür bietet sich eine separate signup(username, password) Funktion an und vielleicht eine dritte Funktion, die überprüft, ob das Passwort alle erforderlichen Kriterien erfüllt. Schreibt lieber mehrere kleine Funktionen als eine Große!

Zusammenfassung: Funktionen sind die Bausteine eurer Programme. Sie verarbeiten Argumente und können dann Daten mit return zurückgeben. return beendet die Ausführung der Funktion sofort und ignoriert Code danach. Funktionen werden entweder über function oder mit dem Arrow => definiert und können danach beliebig oft aufgerufen werden, indem zwei runde Klammern (()) an den Funktionsnamen gehängt werden, zwischen die die Argumente gesetzt werden: meineFunktion(argument1, argument2).

Schleifen (Loops)

Wir wissen jetzt, dass wir Code, den wir mehrfach brauchen in Funktionen stecken und beliebig oft aufrufen können. Aber eine Funktion hundert Mal von Hand aufzurufen ist noch immer nicht besonders effizient. Deshalb gibt es Schleifen, die uns die Arbeit wieder leichter machen.

In JavaScript gibt es zwei Arten von Schleifen: for und while.

While-Schleifen werden ausgeführt, so lang eine Bedingung true ist. Es ist die einfachste Form einer Schleife:

// läuft, bis euer Computer schmilzt – schlechte Idee
while(true) {
  console.log('Hello World!');
}

// besser:
let counter = 0;

while (counter < 10) { // nur true so lang counter kleiner als 10 ist
  console.log('Hello World!');
  counter += 1; // 1 zum Counter hinzufügen, identisch zu counter = counter + 1
}

Jeden Durchlauf einer Schleife nennt man eine Iteration. Der erste Loop in obigem Beispiel führt (theoretisch) unendlich Iterationen aus. Der Zweite exakt 10. While-Loops können, wie auch For-Schleifen, mit break beendet werden, auch wenn ihre Bedingung theoretisch noch true ist.

For-Schleifen sind etwas komplizierter als While-Loops, werden aber deutlich häufiger verwendet. Im Prinzip kann man jeden For-Loop auch als While-Schleife schreiben, aber es ist oft übersichtlicher und weniger Fehleranfällig, wenn man einfach einen For-Loop benutzt:

// loopTest.js
const myArray = ['one', 'two', 'three'];

// Implementierung im While-Loop
let i = 0; // i ist eine konventionelle Abkürzung für 'iteration' und 'index'
while (i < myArray.length) {
  console.log(myArray[i]);
  i += 1;
}

// das gleiche als For-Loop
for (let i = 0; i < myArray.length; i += 1) {
  console.log(myArray[i]);
}

For-Schleifen geben uns die Möglichkeit, direkt eine Zähler-Variable zu definieren, die automatisch nach jeder Iteration verändert wird (meistens mit i += 1 um 1 erhöht). Die Variable kann natürlich heißen, wie ihr wollt, es wird nur aus Konvention heraus i verwendet. Und auch die Bedingung, die zwischen der Deklaration der Zähler-Variable und dem Statement zu ihrer Veränderung führt, kann theoretisch alles mögliche sein, hängt aber in den meisten Fällen mit dem Wert der Zähler-Variablen zusammen.

for (let zählerVariable = wert; bedingung; veränderungDerVariable) {
  // Code der ausgeführt werden soll
  // zählerVariable ist hier verfügbar und hat den aktuellen Wert
}

Wie ihr seht eignen sich For-Loops perfekt, um mit jedem Element eines Arrays etwas zu machen.

Aber Amadeus, es gibt doch Array.forEach()! Das ist viel besser!

Natürlich gibt es Array.forEach(), aber nur mit einem For-Loop hast du die Möglichkeit diesen vorzeitig abzubrechen, oder Elemente zu überspringen:

// breakExample.js
const numbers = [1, 2, 3, 3, 4];
let sum = 0;

for (let i = 0; i < numbers.length; i += 1) {
  const currentNumber = numbers[i];
  const lastNumber = numbers[i - 1];

  if (typeof currentNumber !== 'number') { // wenn die aktuelle Nummer keine Zahl ist
    console.log('Invalid Number Detected!'); // gib einen Fehler aus
    sum = 0; // setze sum zurück
    break; // brich den Loop ab
  }

  // wenn die letzte Zahl gleich ist wie die Aktuelle, überspringe die Iteration
  if (currentNumber === lastNumber) continue;

  sum += currentNumber; // ansonsten addiere die aktuelle Zahl zur Summe
}

console.log(sum); // Ausgabe: 10

// wäre numbers === [1, '2', 3]
// wäre die Ausgabe 'Invalid Number Detected' und 0

Achtung:

continue-Statements sollten eigentlich wann immer möglich durch Conditionals ersetzt werden, da continue den Code oftmals weniger gut verständlich macht. Eslint mit der Airbnb-Konvention wird euch warnen, falls ihr ein continue Statement verwendet.

Das selbe Ergebnis ließe sich auch mit Array.forEach() erreichen, allerdings würden bei einer Liste von 100 Zahlen, bei der die erste ein String ist, trotzdem 100 Iterationen stattfinden, weil Array.forEach() nicht mit break abgebrochen werden kann. In einem For-Loop gäbe es nur eine einzige Iteration.

Außerdem kann man mit einem For-Loop auch zum Beispiel über Strings iterieren, die keine forEach()-Methode haben.

Zusammenfassung: um sich wiederholenden Code, also zum Beispiel den Aufruf einer Funktion für jedes Element in einer Liste, möglichst einfach zu schreiben verwendet man Schleifen (Loops). Es gibt einen while-Loop für einfache Situationen und einen viel häufiger verwendeten for-Loop.

Bedingungen

Inzwischen habt ihr in vielen Beispielen schon gesehen, dass manchmal ein oder mehrere Statements nur dann ausgeführt werden sollen, wenn bestimmte Bedingungen eintreffen. Wie in den meisten anderen Programmiersprachen auch, wird das in JavaScript über if…else-Blöcke geregelt, sogenannte „Conditionals“.

Ihr könnt euch diese wie Wenn…Dann-Sätze vorstellen. Wenn das Statement innerhalb der Klammern nach dem iftrue zurückgibt, wird der Code innerhalb der geschweiften Klammern danach ausgeführt. Ansonsten wird, falls vorhanden, der Code im else-Block ausgeführt:

// conditionals.js
let weather = 'sonnig';

if (weather === 'sonnig') { // wenn weather gleich 'sonnig'
  console.log('Du brauchst keinen Regenschirm.'); // dann dass
} else if (weather === 'windig') { // wenn das erste if nicht zutrifft, wird diese Bedingung überprüft
  console.log('Es ist nicht sonnig, aber es ist zu windig für einen Regenschirm');
} else { // falls kein if oder else if zutrifft, wird dieser Code ausgeführt
  console.log('Du solltest einen Regenschirm mitnehmen.');
}

// Ausgabe: 'Du brauchst keinen Regenschirm'

/* da jeweils nur ein statement in den jeweiligen Blöcken steht, könnte man es
auch so schreiben: */
weather = 'schnee';
if (weather === 'sonnig') console.log('Du brauchst keinen Regenschirm.'); // dann dass
else if (weather === 'windig') console.log('Es ist nicht sonnig, aber es ist zu windig für einen Regenschirm');
else console.log('Du solltest einen Regenschirm mitnehmen.');

// Ausgabe: 'Du solltest einen Regenschirm mitnehmen'

Wie ihr seht, könnt ihr mit else if beliebig viele Bedingungen stellen, bevor der else-Block ausgeführt wird. Manchmal ist es aber sinnvoller in diesen Fällen ein switch-Statement zu verwenden, über das ihr hier mehr lernen könnt.

Tipp:

Sowohl while und for-Schleifen, wie auch if, else if und else- Blöcke lassen sich einzeilig schreiben, wenn sie nur ein Statement beinhalten. Ich empfehle diese Schreibweise aber nur dann, wenn die Zeilen dadurch nicht zu lang werden und die Lesbarkeit nicht leidet.

Die eigentlichen Bedingungen, die ihr in die Klammern nach dem if schreibt werden immer so interpretiert, dass am Ende true oder false herauskommt und der Code im if-Block wird nur dann ausgeführt, wenn true das Ergebnis ist. Das bedeutet, dass if(0) nicht ausgeführt wird, weil Boolean(0) === false, passt also auf, wenn ihr mit Zahlen arbeitet!

Um diese Bedingungen zu schreiben gibt es eine Reihe an Vergleichsoperatoren:

  • === (ist zu 100% gleich): strikter Vergleich, trifft nur zu wenn Wert und Typ identisch sind, z.B:

    > 1 === 1
    true
    > '1' === 1
    false

    Dieser Vergleich sollte dem normalen Vergleich (==) immer vorgezogen werden, außer man weiß, was man tut, da false == 0true, aber false === 0false.

  • !== (ist zu 100% ungleich): strikter Ungleichheits-Vergleich. Das Gegenteil von ===

  • < (ist kleiner): wie in der Mathematik, z.B. 5 < 10true, 10 < 5false

  • > (ist größer): wie in der Mathematik, z.B. 5 > 10false, 10 > 5true

  • <= (ist kleiner oder gleich), z.B. 5 <= 10true, 10 <= 10true

  • => (ist größer oder gleich), z.B. 5 >= 10false, 10 >= 10true

Wollt ihr mehrere Bedingungen verknüpfen, gibt es auch logische Operatoren:

  • && (und): nur true wenn beide Bedingungen zutreffen
  • || (oder): true wenn eine Bedingung zutrifft
  • ! (nicht): negiert den Wert, z.B. !true === falsetrue

Kombiniert ergibt das folgendes:

// multipleConditions.js

const number = 4;

// number ist größer 0 UND number ist kleiner gleich 4 ODER der ganzzahlige Rest von number / 2 ist 0
if ((number > 0 && number <= 4) || number % 2 === 0) { // % ist der Modulo-Operator, er gibt immer den Rest einer Division zurück
  console.log('Die Zahl ist gerade, oder kleiner gleich 4 und größer als 0');
}

/* Achtung:
(number > 0 && number <= 4) || number % 2 === 0 ist etwas Anderes als
number > 0 && (number <= 4 || number % 2 === 0)
*/

Achtung:

Die Verknüpfung mehrerer logischer Operatoren kann sehr schnell sehr verwirrend werden. Benutzt deshalb immer Klammern um Elemente zusammenzusetzen, die zusammen gehören. Wie in der Mathematik werden Klammern von innen nach außen aufgelöst. Auch hier wird euch euer Linter helfen indem er euch warnt.

Wenn ihr euch Code von anderen Entwicklern anseht, werdet ihr manchmal über folgendes stolpern, zum Beispiel, wenn einer Variable ein Wert anhand einer Bedingung zugewiesen wird, oder eine Arrow-Function kurz gehalten werden soll:

> const stars = 4;
> const cheese = stars === 5 ? 'yummy' : 'gross';
> const rateCheese = (rating) => rating !== 5 ? console.log('You savage!') : console.log('You know what’s up!');
> rateCheese(3);
You savage!
> rateCheese(5);
You know whats up!

In manchen Fällen macht das Sinn, aber vor allem für den Anfang werdet ihr euch leichter tun euren Code im Nachhinein zu verstehen, wenn ihr die if…else- Statements ausschreibt:

// sameAsAbove.js
const stars = 4;
let cheese;

if (stars === 5) cheese = 'yummy';
else cheese = 'gross';

const rateCheese = (rating) => {
  if (rating !== 5) console.log('You savage!');
  else console.log('You know what’s up!');
}

rateCheese(3); // Ausgabe: You savage!
rateCheese(5); // Ausgabe: You know what’s up!

Zusammenfassung: Wenn…Dann-Sätze könnt ihr in JavaScript mit if…else Blöcken beschreiben. if-Blöcke werden nur ausgeführt, wenn die Bedingung in den Klammern true ist. else-Blöcke sind optional. In seltenen Fällen werden Conditionals auch mit ? geschrieben, aber das sollte nur von erfahreneren Entwicklern verwendet werden.

Jetzt kennt ihr die Grundkonzepte von JavaScript und des Programmierens allgemein. Aber wie wendet man diese nun an, um ein Programm zu entwickeln? Damit geht es jetzt weiter.

Wie entwickelt man ein Programm?

Ein Programm zu entwickeln ist eigentlich einfach. Ihr müsst nur genau wissen, was das Programm machen soll und welche Daten es dafür braucht. Dann entwickelt ihr einen Algorithmus (oder sucht nach einem Bestehenden) und übersetzt diesen in die Programmiersprache eurer Wahl.

Dann gebt ihr die benötigten Daten ein und das Programm gibt euch den programmierten Output zurück. Easy peasy! 😉

Die Sache mit dem Kartenstapel

Nehmen wir als Beispiel das Mischen eines Kartenstapels. Wenn wir mit unserem ungemischten Kartenstapel zu einer x-beliebigen Person gehen und diese bitten, ihn zu mischen sagen Manche vielleicht „Ich kann nicht mischen“ und breiten den Stapel vor sich auf dem Tisch aus und schieben die Karten hin und her, Andere nehmen den Stapel selbstbewusst an sich und mischen ihn mit den ausgefallensten Tricks, und wieder Andere mischen ihn einfach, ohne groß darüber nachzudenken. Tatsache ist, dass wir am Ende auf die eine oder andere Art und Weise einen mehr oder weniger gemischten Kartenstapel zurückbekommen werden.

Geben wir die selbe Aufgabe einem Computer, wird er sofort anfangen mit Fehlermeldungen um sich zu werfen:

  • Was ist ein Kartenstapel?
  • Was ist eine Karte?
  • Was heißt denn bitte „mischen“?
  • Was willst du von mir?

Natürlich werden die Fehlermeldungen nicht so aussehen, aber ihr versteht hoffentlich, worauf ich hinauswill: der Computer hat keine Ahnung davon, was ein Kartenstapel ist, und weiß auch nicht, was „mischen“ sein soll. Es ist, als hätten wir einem Kleinkind unseren Stapel gegeben (wobei das Kleinkind es wahrscheinlich trotzdem irgendwie schaffen würde, den Stapel zu mischen 😉).

Der Computer hat nur ein sehr, sehr grundlegendes Verständnis von der Welt, wie wir sie kennen, bzw. haben schon andere, viel begabtere Entwickler als wir Konstrukte geschaffen, um dem Computer dieses Verständnis beizubringen – die sogenannten Programmiersprachen. Ein Computer kann zum Beispiel gut zwischen „Wahr“ und „Falsch“ unterscheiden. Ein Computer hat auch ein Konzept von Listen (Arrays) und Objekten (Sammlungen von Daten). Ein Computer weiß auch, dass es Dinge gibt, die man Ausführen kann: Funktionen.

Unsere Aufgabe ist es jetzt unser Beispiel mit dem Kartenstapel in diese sehr rudimentären Begriffe zu übersetzen und dem Computer Schritt für Schritt zu erklären wie man einen Kartenstapel mischt. Diese Anleitung nennt man einen Algorithmus.

Aber Amadeus, das ist doch total umständlich, ich mische meinen Kartenstapel lieber selbst, dann bin ich viel schneller fertig!

Das stimmt natürlich, aber was, wenn du 50 Kartenstapel mischen musst? Oder 100? Der gewaltige Vorteil eines Computers ist, dass er einen guten Algorithmus in einer für einen Menschen unerreichbaren Geschwindigkeit ausführen kann. Natürlich sind nicht alle Algorithmen gleich gut, aber selbst mit einem mittelmäßigen Misch-Algorithmus hätte der Computer 100 Kartenstapel gemischt, bevor du deinen überhaupt in die Hand genommen hättest.

Einen Algorithmus könnt ihr euch wie ein Kochrezept vorstellen: ihr gebt dem Computer eine Liste an Zutaten, eine genaue Anleitung, was mit den Zutaten zu machen ist und der Computer gibt euch am Ende das Ergebnis des Rezepts zurück.

Wie entwickelt man also einen solchen Algorithmus? Nehmen wir unser Beispiel. Es geht nur darum die Karten zu mischen, also ist es für den Computer nicht wichtig, was genau eine Karte ist. Wir könnten sie also einfach als eine Zahl repräsentieren, oder als ein Objekt, dass den Kartennamen hält, oder eine Zeichenkette, die die Karte beschreibt – wie immer gibt es unendlich viele Möglichkeiten. Wir entscheiden uns in diesem Fall für ein Objekt (auch wenn eine Zahl effizienter wäre). Ein Stapel Karten ist einfach eine Liste von Objekten mit einer bestimmten Reihenfolge, dafür bietet sich also ein Array an.

Jetzt weiß der Computer also, was wir mit einem Stapel Karten meinen. Der nächste Schritt ist es, dem Computer zu erklären, was „mischen“ bedeutet und dafür können wir uns einfach ansehen, wie die meisten Menschen mischen:

Sie nehmen den Stapel in eine Hand, nehmen sich eine zufällige Zahl von Karten vom Anfang des Stapels, aber niemals alle Karten, denn das würde keinen Sinn machen, und legen diesen kleineren Stapel einfach auf das Ende des verbleibenden Stapels. Diesen Vorgang wiederholen sie beliebig oft, aber natürlich nicht unendlich oft und auch nicht nur ein oder zwei Mal, denn dann wäre der Stapel ja nicht wirklich gemischt.

Natürlich gibt es auch Menschen, die den Stapel auf einem Tisch ausbreiten und die Karten hin und her schieben und dann wieder zu einem Stapel formen, oder Menschen, die den Stapel in kleinere Stapel aufteilen und diese dann ineinander Schieben, sodass sich praktisch immer eine Karte aus einem Stapel über eine Karte des anderen Stapels mischt, aber am Ende kommt immer ein gemischter Stapel dabei heraus. Genauso gibt es viele verschiedene Algorithmen, um das gewünschte Ergebnis zu erreichen, wir müssen uns nur für einen entscheiden.

Ein konkreter Algorithmus für unser Beispiel nach der ersten Mischmethode:

Zutaten:

  • Ein Array aus Kartenobjekten

Anweisungen:

  • Nimm eine zufällige Anzahl an Kartenobjekten, die geringer als die Gesamtanzahl von Karten ist, vom Anfang des Arrays und stecke sie ans Ende des Arrays
  • Wiederhole diesen Vorgang eine zufällige Anzahl von Malen, aber mindestens fünf und maximal 25 mal
  • Gib das veränderte Array zurück

Dieser Algorithmus ist sicher nicht der Effizienteste oder Beste, aber für den Anfang funktioniert er. Wie man auf Englisch so schön sagt: it gets the job done. Optimieren und verbessern kann man im Nachhinein noch immer. Wichtig ist, dass der Algorithmus deterministisch ist, also dass er bei gleichem Input immer der gleiche Output generiert wird (bei Zufallszahlen ist das natürlich schwierig, aber unser Algorithmus ist dennoch deterministisch, denn wenn wir die Zufallszahlen auf feste Werte setzen, werden wir am Ende immer die selbe „gemischte“ Reihenfolge erhalten).

Hat man erst einmal einen Algorithmus, muss man diesen nur noch in eine Programmiersprache übersetzen und man hat ein fertiges Programm, das ein Computer ausführen kann (in diesem Fall eine Funktion, damit wir sie immer wieder verwenden können):

function shuffleDeck() {
  // Ein Array aus Kartenobjekten
  let cards = [{ name: 'König' }, { name: 'Dame' }, { name: 'Bube' }, { name: 'Ass' }];
  // Eine zufällige Zahl zwischen 5 und 25
  const iterations = Math.floor(Math.random() * 21) + 5 // Math.random() gibt eine zufällige Zahl zwischen 0 und 1 aus, z.B. 0.2134

  // eine Schleife, die so lange ausgeführ wird, wie die Variable i kleiner ist als die variable iterations
  for (let i = 0; i < iterations; i += 1) {
    const cardsToGrab = Math.floor(Math.random() * (cards.length - 1)) + 1; // Zufällige Zahl, die immer 1 kleiner ist als die Gesamtanzahl Karten
    cards = cards.concat(cards.splice(0, cardsToGrab));
  }

  // gibt die gemischten Karten zurück
  return cards;
}

Für viele Probleme haben andere Menschen schon Algorithmen gefunden, die nahezu Perfekt sind, deshalb wird es recht selten vorkommen, dass ihr einen Algorithmus neu erfinden müsst. Für unser obiges Problem könntet ihr also einfach Google fragen und würdet wahrscheinlich recht schnell auf den Fisher-Yates shuffle stoßen. Grob zusammengefasst geht dieser Alogrithmus rückwärts durch den Kartenstapel und tauscht immer die „aktuelle“ Karte mit einer zufällig ausgewählten Karte, die vorher im Stapel auftaucht:

function fisherYatesShuffle() {
  // Ein Array aus Kartenobjekten
  const cards = [{ name: 'König' }, { name: 'Dame' }, { name: 'Bube' }, { name: 'Ass' }];

  // Schleife, die rückwärts durch das Array geht
  for (let currentIndex = cards.length - 1; currentIndex > 0; currentIndex -= 1) {
    // Zufälliger index zwischen 0 (erste Karte) und dem aktuellen Index
    const randomCardIndex = Math.floor(Math.random() * (currentIndex + 1));
    // Aktuelle Karte
    const currentCard = cards[currentIndex];

    // Tausche die Karten
    cards[currentIndex] = cards[randomCardIndex];
    cards[randomCardIndex] = currentCard;
  }

  // gibt die gemischten Karten zurück
  return cards;
}

Dieser Algorithmus ist schwieriger zu verstehen als unser Einfacher von zuvor, allerdings in einigen Bereichen deutlich überlegen:

  1. Er hat eine deutlich größere Zufälligkeit, d.h. die Wahrscheinlichkeit mit der selben Reihenfolge zu enden, mit der man angefangen hat, ist kleiner
  2. Er funktioniert bei allen möglichen Kartenstapeln, während unser Algorithmus Schwierigkeiten bei kleinen und großen Kartenstapeln hat, weil die Qualität des gemischten Stapels von der Anzahl der Karten und der Anzahl der Iterationen abhängt, die Karten werden also in den meisten Fällen nicht besonders gut gemischt
  3. Er ist deutlich effizienter, denn mit unserer Lösung müssen wir unsere Schleife mindestens 5 und maximal 25 Mal ausführen, dieser Algorithmus führt die Schleife immer genau so oft aus, wie Karten im Stapel sind (mal abgesehen davon, dass die Funktionen Array.splice() und Array.concat() aufwendiger auszuführen sind als ein einfacher Tausch)

Wie ihr seht lohnt es sich also, erst einmal zu recherchieren, wie man etwas macht, bevor man seine eigene Lösung entwickelt. Glaubt mir, richtige Entwickler verbringen Stunden auf Seiten wie StackOverflow. 😉

Trotzdem kommt es natürlich immer wieder vor, dass ihr vor einem Problem steht, das entweder noch nicht gelöst wurde, oder das so spezifisch ist, dass ihr keine allgemeingültigen Lösungen dafür finden könnt. Dann müsst ihr eure eigenen Lösungen entwickeln und solltet dabei darauf achten die einfachste Lösung zu wählen, die das Problem für euch löst. Optimieren kann man nacher immer noch, wenn es zu Performance-Problemen kommen sollte, aber zunächst ist es wichtig, dass eure Lösung überhaupt funktioniert.

Überlegt euch, wie ihr als Mensch das Problem lösen würdet und übersetzt das in eine für den Computer verständliche Form.

Wie geht man mit Fehlermeldungen um?

Irren ist menschlich, hat ein alter Römer einmal gesagt. Egal wie gut ihr programmieren könnt, ihr werdet immer Fehler machen – und das ist okay. Man kann viel von seinen Fehlern lernen, wie wir ja schon besprochen haben, als es um Linter ging.

Wie viele andere Programmiersprachen auch, versucht JavaScript euch möglichst nützliche Fehlermeldungen auszugeben:

/Pfad/zur/Datei/breakExample.js:6
for (let i = 0; i < Numbers.length; i += 1)
                    ^

ReferenceError: Numbers is not defined
    at Object.<anonymous> (/home/amadeus/Git/hm-webdesign/content/examples/breakExample.js:6:21)

In diesem Beispiel habe ich mich vertippt und statt numbersNumbers geschrieben. Beim Ausführen der Datei mit node Pfad/zur/Datei/breakExample.js hat ist Node auf den Fehler gestoßen, hat die Ausführung des Programms abgebrochen und mir die Fehlermeldung ausgegeben.

Diese Fehlermeldung besteht aus mehreren Teilen:

  1. Der Angabe der Datei und der Zeile in der Datei in der der Fehler auftrat
  2. Wo genau in der Zeile der Fehler auftrat
  3. Was für ein Fehler aufgetreten ist, in diesem Fall ein ReferenceError, da „Numbers“ nicht deklariert wurde
  4. Eine sogenannte Stack-Trace, also der Aufruf welcher Funktionen letztendlich zum Auftreten ds Fehlers geführt hat. In diesem konkreten Fall war es nur die Datei selbst, aber später einmal werden diese Traces länger werden, wenn ihr zum Beispiel mit Libraries arbeitet. In speziellen Fällen kann das nützlich sein, aber für euch relevanter sind die ersten drei Punkte und natürlich vor allem die Fehlermeldung selbst, denn die könnt ihr googeln, falls ihr sie nicht versteht. 😉

Wie ich bereits angemerkt habe, bricht JavaScript das komplette Programm ab sobald ein Fehler auftritt. Das ist natürlich nicht immer ideal, weshalb ihr für bestimmte Situationen try…catch-Blöcke verwenden könnt, um die Fehlermeldungen „abzufangen“. Das solltet ihr allerdings erst dann tun, wenn ihr wisst, dass euer Code einwandfrei funktioniert und die Fehlermeldungen deshalb auftreten können, weil evtl. keine Internetverbindung besteht oder Ähnliches, das außerhalb eurer Kontrolle liegt.

Wir werden uns weiter mit Fehlerbehandlung beschäftigen, wenn wir über Datenbanken sprechen, aber falls ihr jetzt schon neugierig seid, könnt ihr hier mehr darüber erfahren.

Was ist eine Dokumentation und wie liest man die?

Bei solch komplexen Systemen wie Programmiersprachen, aber auch Bibliotheken, Frameworks und Libraries für eine Programmiersprache wie JavaScript ist es unabdingbar, dass neue und bestehende Entwickler einen guten Ein- und Überblick über alle Details erlangen können. Zu diesem Zweck existieren die sogenannten Dokumentationen, die erklären, wie eine Sprache, Library, etc. verwendet werden kann, wie sie funktioniert und welche Funktionen sie dem Programmierer an die Hand gibt.

Wenn ihr schon etwas mehr mit DevDocs gearbeitet habt, habt ihr bereits gesehen, wie diese Dokumentationen aufgebaut sind. Ihr könnt einfach nach einem Modul, oder einer Funktion suchen und erhaltet sofort alle relevanten Informationen.

So könnt ihr zum Beispiel in DevDocs oder direkt im MDN nach Math suchen, dem Modul für mathematische Funktionen in JavaScript und werdet schnell erkennen können, welche Funktionen es in diesem Modul gibt und wie ihr sie verwenden könnt.

Ein Screenshot für die Dokumentation von Math.random

Dokumentationen sind neben dem Quellcode von Anwendungen die beste Möglichkeit für euch zu lernen, aber es macht natürlich keinen Sinn, eine solche Dokumentation von Vorne bis Hinten durchzulesen, viele der Funktionen braucht ihr nur selten oder gar nicht und es würde euch nur verwirren.

Gute Frameworks und Libraries wie Vue geben euch deshalb sogenannte „Guides“ an die Hand, die euch Schritt für Schritt an die Library heranführen, sowie eine API-Dokumentation, in der ihr schnell eine bestimmte Funktion nachschlagen könnt.

Bei kleineren Projekten, Modulen, oder Libraries findet ihr diese Informationen meistens einfach in der README.md Datei im jeweiligen Git-Repository.

Es macht Sinn, sich durch diese Guides zu arbeiten, wenn man eine Library das erste Mal verwendet, oder für ein Projekt evaluiert. Ansonsten empfehle ich euch, nur die Teile der Dokumentation zu lesen, die für euch gerade relevant sind.

Wenn ihr zum Beispiel die Aufgabe habt, in JavaScript eine zufällige Zahl zu generieren und ihr nicht wisst wie, sollte eure erste Anlaufstelle Google sein – bitte behaltet dabei im Kopf, dass Programmieren auf Englisch geschieht und deshalb fast alles auf Englisch ist, ihr also auch auf Englisch googeln solltet.

Google wird euch höchstwahrscheinlich die Funktion Math.random ausspucken, und wahrscheinlich auch eine Anleitung und ein Tutorial, wie sie zu benutzen ist. Für später ist das super, aber jetzt wo ihr noch lernt ist es besser, wenn ihr die Anleitungen ignoriert, DevDocs aufmacht und euch dort die Dokumentation zu Math.random durchlest und dann selbst versucht, euer Problem mit diesen Informationen zu lösen. Dadurch erzielt ihr einen größeren Lerneffekt und wie wir gelernt haben: Verbessern kann man später immer noch.

Tipp:

Anstatt Google könnt ihr natürlich gerne auch immer mich fragen, aber Google wird euch wahrscheinlich schneller ein Ergebnis liefern. 😉 Außerdem weiß ich auch nicht alles auswendig – das muss auch niemand. Fragt also für einfache Fragestellungen eher Google, dann erspart ihr es mir für euch googeln zu müssen und ich habe mehr Zeit, euch die Dinge konkreter und tiefergehender zu erklären, falls es einmal Verständnisschwierigkeiten geben sollte, oder ihr einfach mehr zu einem Thema erfahren wollt. 😊

Praxis

Wir haben heute sehr viele Beispiele und Konzepte besprochen. Jetzt ist es wichtig, dass ihr all dieses neue Wissen ausprobieren könnt, damit ihr es verinnerlicht. Viele dieser Konzepte sind schließlich recht abstrakt und man kann sie nur dann wirklich verstehen, wenn man sie selbst benutzt.

In diesem Sinne möchte ich, dass ihr erst einmal durch die Beispiele von heute geht und sie einfach mal selbst schreibt und versucht genau zu verstehen, was passiert. Besonders wichtig ist dabei meiner Meinung nach, dass ihr die Code- Zeilen selbst abtippt und nicht einfach kopiert und wieder einfügt, auch wenn die Versuchung dafür groß sein mag. Viele der heute besprochenen Statements werdet ihr noch viele weitere Male brauchen, deshalb macht es Sinn auch eure Finger schon einmal daran zu gewöhnen, sie zu tippen.

Für kurze Kommandos und einzelne Statements empfehle ich euch den interaktiven Modus von Node, den ihr einfach mit der Eingabe node und Enter erreichen könnt. Für mehrzeilige Programme, kleine Algorithmen, mehrere Statements, etc. empfehle ich euch, *.js Dateien anzulegen. Diese könnt ihr ausführen, indem ihr node pfad/zur/datei.js eingebt.

Falls etwas unklar sein sollte, fragt einfach nach.

Das Zahlenratespiel

Um das heute gelernte Wissen weiter zu vertiefen, möchte ich, dass ihr ein kleines Zahlenratespiel entwickelt. Hierbei soll sich der Computer eine zufällige Zahl in einem vorgegebenen Rahmen „ausdenken“, die ihr dann über möglichst wenige Eingaben erraten sollt. Wann immer ihr eine Zahl eingebt, die nicht die „ausgedachte“ Zahl ist, gibt der Computer euch einen Tipp, ob die Zahl größer oder kleiner ist, als die von euch eingegebene.

Hier ein paar Hilfestellungen dazu:

  • Math.random() gibt euch eine zufällige Zahl zwischen 0 und 1, wobei sie niemals 1 ist (1 exklusiv). Ein Aufruf von Math.random() gibt euch also zum Beispiel 0.7947725165815829 zurück

  • Der folgende Code erlaubt es euch, eine Benutzereingabe im Terminal anzufordern und zu verarbeiten:

    // importiere die 'readline'-Library und initialisiere sie
    const input = require('readline').createInterface({
      input: process.stdin, // input und output werden vom Terminal eingelesen
      output: process.stdout,
      prompt: 'Your answer: ', // das Prefix, das angezeigt wird, wenn input.prompt() ausgeführt wird
    });
    
    // definiere einen Event Listener, also eine Funktion, die immer dann
    // aufgerufen wird, wenn das „line“-Event auftritt.
    // Das passiert immer dann, wenn nach input.prompt() Enter gedrückt wird.
    // „answer“ beinhaltet den eingegebenen Text ohne das Prefix.
    input.on('line', (answer) => {
      // do something
      // z.B. console.log(answer)
    });
    
    // mit input.close() könnt ihr das Programm wieder beenden, ansonsten die
    // Ausführung im Terminal mit Ctrl+C abbrechen. Das passiert, weil das
    // readline-Interface in der input-Variablen solange auf Input wartet, bis
    // es mit .close() geschlossen wird
    
    input.prompt(); // jetzt wird im Terminal 'Your answer: ' ausgegeben und auf eine Eingabe gewartet

    Da er recht komplex ist, ist es für den Augenblick nicht wichtig, wie genau er funktioniert. Alles was ihr wissen müsst ist, dass der Aufruf input.prompt() die Zeile 'Your answer: ' ausgibt und dann darauf gewartet wird, dass eine Eingabe erfolgt, die mit Enter bestätigt wird. Dann wird die Funktion aufgerufen, die ihr in input.on('line') definiert hat. Diese Funktion bekommt den eingegebenen Text als Argument übergeben und ihr könnt ihn dann weiterverarbeiten.

  • Überlegt euch einen geeigneten Algorithmus, bevor ihr darauf los programmiert. Dann wird es euch leichter fallen den eigentlichen Code zu schreiben.

Die Aufgabe ist nicht die Einfachste für den Anfang, aber ich stehe euch gerne für Fragen und weitere Hilfestellungen zur Verfügung.

Weitere Gestaltung

Ihr solltet inzwischen einige Screens eurer ToDo-Anwendung ausgestaltet haben. Wenn ihr für diese gerne Feedback hättet, lasst es mich wissen. Wenn ihr in Figma gearbeitet habt, dann fügt mich zu eurer Datei hinzu, damit ich dort direkt Kommentare hinterlassen kann. Ansonsten könnt ihr mir aber natürlich auch gerne einfach Bilder zukommen lassen, die ich dann als Basis für mein Feedback benutzen werde.

Falls ihr es noch nicht getan habt, ist nun auch ein guter Zeitpunkt, sich über die Responsivität der App Gedanken zu machen. Überlegt euch, wie sie auf verschiedenen Bildschirmen aussehen kann und gestaltet eine Desktop-Version aus.


In der nächsten Session werden wir mehr über JavaScript an sich lernen und uns ansehen, wie JavaScript und HTML/CSS zusammengehören und miteinander interagieren können. 🎉