Übersicht
JavaScript ist die Programmiersprache des Web-Frontends. Sie läuft in jedem modernen Browser und — dank Node.js — auch auf dem Server. Ursprünglich 1995 in nur 10 Tagen von Brendan Eich bei Netscape entwickelt, ist JavaScript heute die verbreitetste Programmiersprache weltweit.
Historie
JavaScript wurde 1995 als „Mocha" bei Netscape entwickelt, umbenannt zu „LiveScript" und schließlich zu „JavaScript" — obwohl es technisch nichts mit Java zu tun hat. Die Standardisierung erfolgt seit 1997 als ECMAScript durch die ECMA International. Neue Versionen (ES2015 aka ES6, ES2020, ES2024 …) erweitern die Sprache jährlich.
Warum JavaScript?
JavaScript ist die einzige Sprache, die nativ in jedem Browser läuft. Für alles Interaktive im Web führt kein Weg an JavaScript (oder TypeScript, das zu JS kompiliert) vorbei. Mit Node.js ist JavaScript auch auf dem Server ein First-Class-Citizen und ermöglicht Full-Stack-Entwicklung in einer einzigen Sprache.
Installation
JavaScript benötigt keine Installation — es läuft in jedem modernen Browser. Für Server-seitige Entwicklung: Node.js von nodejs.org installieren.
Hello World
Das minimalste vollständige Programm in JavaScript:
console.log('Hello, World!');
Variablen & Datentypen
↗ Eigene SeiteDiese Sektion zeigt, wie du Variablen mit const, let und var deklarierst, welche Datentypen JavaScript kennt und wie sich Primitive von Objekt-Referenzen unterscheiden.
Variablen speichern Werte unter einem Namen. In modernem JavaScript deklarierst du sie mit const oder let. var findest du fast nur noch in älterem Code. Den Datentyp hat der Wert, nicht die Variable, denn JS ist dynamisch typisiert.
const, let und var
Mit const kannst du einer Variablen keinen neuen Wert zuweisen, mit let schon. Beide gelten nur innerhalb ihres Blocks. var gilt in der ganzen Funktion und wird gehoisted. Das führt oft zu Fehlern.
const max = 10;
let count = 0;
count += 1; // ok
// max = 20; // TypeError
if (true) {
let inner = 'nur hier';
}
// inner // ReferenceError
Best Practice: Nimm standardmäßig const. let brauchst du nur, wenn du wirklich neu zuweist.
Primitive Datentypen
Es gibt sieben primitive Typen: string, number, bigint, boolean, undefined, null und symbol. Primitive sind unveränderlich und werden als Wert kopiert.
const name = `Ada`;
const pi = 3.14;
const big = 9007199254740993n;
const aktiv = true;
let leer; // undefined
const nichts = null;
const id = Symbol('id');
Typen prüfen mit typeof
Der Operator typeof gibt den Typ als String zurück. Bei null und Arrays brauchst du eigene Prüfungen.
typeof 'Hallo'; // 'string'
typeof 42; // 'number'
typeof undefined; // 'undefined'
typeof null; // 'object' (historischer Bug!)
typeof []; // 'object'
typeof (() => {}); // 'function'
Array.isArray([]); // true
value === null; // korrekte null-Prüfung
Objekte und Referenzen
Alles, was kein Primitiv ist, ist ein Objekt. Das gilt auch für Arrays und Funktionen. In der Variablen steht dann eine Referenz auf das Objekt. const schützt nur die Bindung, nicht den Inhalt.
const user = { name: 'Ada' };
const alias = user;
alias.name = 'Grace';
console.log(user.name); // 'Grace'
const kopie = { ...user }; // flache Kopie
const tief = structuredClone(user); // tiefe Kopie
Fallstrick: Object.freeze() friert nur die oberste Ebene ein. Verschachtelte Objekte kannst du weiterhin ändern.
Operatoren & Ausdrücke
↗ Eigene SeiteDiese Sektion erklärt die wichtigsten Operatoren in JavaScript: Arithmetik, Vergleiche, logische und Nullish-Operatoren, Optional Chaining und Zuweisungskurzformen. Außerdem geht es darum, wie Ausdrücke ausgewertet werden.
Ein Ausdruck ist jedes Stück Code, das einen Wert ergibt, zum Beispiel 2 + 3, user.name oder fn(). Operatoren verknüpfen Werte zu neuen Ausdrücken. Wer ihre Regeln kennt, besonders bei Typumwandlung und Kurzschlussauswertung, vermeidet viele typische Bugs.
Arithmetik und Zuweisung
Neben + - * / gibt es % (Rest) und ** (Potenz). Vorsicht: + verkettet Strings, sobald einer der Operanden ein String ist.
const a = 7 % 3; // 1
const b = 2 ** 10; // 1024
const c = '5' + 1; // '51' (String!)
const d = '5' - 1; // 4 (Zahl)
let count = 0;
count += 5; // 5
count++; // 6
Vergleiche: === statt ==
== wandelt Typen vor dem Vergleich um und liefert dadurch überraschende Ergebnisse. Nimm standardmäßig === und !==. Objekte werden über ihre Referenz verglichen, nicht über ihren Inhalt.
0 == ''; // true (Typumwandlung)
0 === ''; // false
null == undefined; // true
[1] === [1]; // false (verschiedene Referenzen)
Number.isNaN(NaN); // true – NaN === NaN ist false
Logische Operatoren, ?? und ?.
&& und || werten per Kurzschluss aus und geben einen der Operanden zurück, nicht zwingend true oder false. ?? greift nur bei null oder undefined. Dadurch bleiben gültige Werte wie 0 oder '' erhalten. ?. bricht den Zugriff ab, wenn ein Zwischenwert fehlt, und ergibt dann undefined.
const config = { retries: 0 };
const r1 = config.retries || 3; // 3 – 0 ist falsy
const r2 = config.retries ?? 3; // 0 – korrekt
const city = user?.address?.city ?? 'unbekannt';
const result = api.fetch?.(); // nur aufrufen, wenn vorhanden
// Logische Zuweisung (ES2021)
config.timeout ??= 5000;
isReady &&= checkState();
Hinweis: ?? darf nicht ohne Klammern mit && oder || kombiniert werden, sonst gibt es einen SyntaxError.
Ternärer Operator und Rangfolge
bedingung ? a : b ist ein Ausdruck und passt deshalb in Template Literals oder Rückgabewerte. Verschachtelte Ternaries werden schnell unleserlich. Im Zweifel lieber if verwenden oder mit Klammern arbeiten.
const label = `${items.length} ${items.length === 1 ? 'Eintrag' : 'Einträge'}`;
const x = 2 + 3 * 4; // 14 – * vor +
const y = (2 + 3) * 4; // 20
const z = typeof null; // 'object' (historischer Fehler)
Best Practice: Setze Klammern, wo die Rangfolge nicht sofort klar ist. Sie kosten nichts und machen die Absicht eindeutig.
Kontrollstrukturen
↗ Eigene SeiteDiese Sektion behandelt Verzweigungen (if, switch) und Schleifen (for, for...of, for...in, while) in JavaScript, dazu break, continue und den Einsatz von await in Schleifen.
Kontrollstrukturen legen fest, welche Anweisungen wann und wie oft ausgeführt werden. In JavaScript sind das vor allem Verzweigungen (if, switch) und Schleifen (for, while, for...of).
if / else
Die Bedingung wird in einen Boolean umgewandelt. 0, "", null, undefined und NaN gelten als falsy.
const alter = 17;
if (alter >= 18) {
console.log('volljährig');
} else if (alter >= 16) {
console.log('fast');
} else {
console.log('minderjährig');
}
switch
switch vergleicht strikt (===). Ohne break läuft die Ausführung in den nächsten case weiter (Fall-through).
const status = 404;
switch (status) {
case 200:
console.log('OK');
break;
case 401:
case 403:
console.log('Kein Zugriff');
break;
default:
console.log(`Status ${status}`);
}
Schleifen
Für Arrays und andere Iterables ist for...of die erste Wahl. for...in iteriert über die Schlüssel eines Objekts, auch über geerbte. Für Arrays ist es deshalb ungeeignet.
const farben = ['rot', 'grün', 'blau'];
for (const [i, farbe] of farben.entries()) {
console.log(i, farbe);
}
const user = { name: 'Ada', rolle: 'admin' };
for (const key in user) {
console.log(key, user[key]);
}
while prüft die Bedingung vor jedem Durchlauf, do...while erst danach. Dort läuft der Rumpf also mindestens einmal.
break, continue und await
break beendet die Schleife, continue springt zum nächsten Durchlauf. Anders als bei forEach funktioniert await in echten Schleifen und arbeitet die Schritte nacheinander ab.
const urls = ['/a', '/b', '/c'];
for (const url of urls) {
const res = await fetch(url);
if (!res.ok) continue;
const data = await res.json();
if (data.done) break;
}
Tipp: Deklariere Schleifenvariablen mit let oder const. Dann bekommt jeder Durchlauf eine eigene Bindung, und typische Closure-Fehler bleiben aus.
Schleifen & Iteration
↗ Eigene SeiteDiese Sektion zeigt die Schleifenformen in JavaScript: for, while, for…of, for…in und for await…of. Dazu kommen break, continue und Labels sowie typische Fallstricke.
Welche Schleife passt, hängt davon ab, worüber du iterierst: über Indizes, über die Werte eines Iterables, über die Schlüssel eines Objekts oder über asynchrone Daten.
for und while
Die klassische for-Schleife nimmst du, wenn du den Index brauchst. while prüft die Bedingung vor jedem Durchlauf. do…while läuft mindestens einmal.
for (let i = 0; i < 3; i++) {
console.log(i); // 0, 1, 2
}
let n = 3;
while (n > 0) n--;
Verwende let statt var. Nur mit let bekommt jede Iteration eine eigene Bindung. Das ist wichtig bei Closures, zum Beispiel in setTimeout.
for…of vs. for…in
for…of durchläuft die Werte von Iterables wie Array, String, Map, Set oder NodeList. for…in durchläuft die aufzählbaren Schlüssel eines Objekts, auch geerbte. Für Arrays ist for…in deshalb ungeeignet.
const farben = ['rot', 'grün'];
for (const [i, farbe] of farben.entries()) {
console.log(`${i}: ${farbe}`);
}
const user = { name: 'Ada', rolle: 'admin' };
for (const [key, value] of Object.entries(user)) {
console.log(`${key} = ${value}`);
}
Object.entries() zusammen mit for…of ist meist sauberer als for…in, weil nur eigene Eigenschaften vorkommen.
break, continue und Labels
break beendet die Schleife. continue springt direkt zur nächsten Iteration. Mit einem Label kannst du auch verschachtelte Schleifen gezielt verlassen.
aussen: for (const zeile of matrix) {
for (const zelle of zeile) {
if (zelle === null) continue;
if (zelle === 0) break aussen;
}
}
Achtung: forEach() kennt kein break. Wenn du vorzeitig abbrechen willst, nimm for…of, some() oder find().
Asynchron: await in Schleifen
await in for…of arbeitet die Schritte nacheinander ab. Für asynchrone Iterables wie Streams oder Async-Generatoren gibt es for await…of.
async function* zaehler() {
yield 1;
yield 2;
}
for await (const wert of zaehler()) console.log(wert);
// parallel statt nacheinander:
const daten = await Promise.all(urls.map((u) => fetch(u)));
Ein häufiger Fehler: await in einem forEach()-Callback wartet nicht. Die Schleife läuft sofort weiter.
Funktionen & Closures
↗ Eigene SeiteDiese Sektion behandelt die Syntaxformen von Funktionen in JavaScript, also Deklaration, Ausdruck und Arrow Function, dazu Parameter-Features und Closures. Außerdem geht es um typische Fallstricke mit Gültigkeitsbereich und this.
In JavaScript sind Funktionen normale Werte. Du kannst sie Variablen zuweisen, als Argument übergeben und aus anderen Funktionen zurückgeben. Eine Closure entsteht, wenn eine Funktion Variablen aus dem Bereich verwendet, in dem sie definiert wurde. Diese Variablen bleiben erhalten, auch wenn die äußere Funktion schon beendet ist.
Deklaration, Ausdruck, Arrow Function
Funktionsdeklarationen werden gehoistet. Du kannst sie also schon vor ihrer Definition aufrufen. Bei Funktionsausdrücken und Arrow Functions geht das nicht. Arrow Functions haben außerdem kein eigenes this und kein arguments.
function add(a, b) { return a + b; }
const mul = function (a, b) { return a * b; };
const square = (x) => x * x; // impliziter Rückgabewert
const toObj = (id) => ({ id }); // Objektliteral in Klammern!
Parameter: Defaults, Rest, Destrukturierung
Standardwerte greifen nur bei undefined, nicht bei null. Rest-Parameter sammeln die übrigen Argumente in einem echten Array.
const greet = (name = 'Welt', ...rest) =>
`Hallo ${name}! (${rest.length} weitere)`;
const connect = ({ host = 'localhost', port = 80 } = {}) =>
`${host}:${port}`;
connect({ port: 8080 }); // "localhost:8080"
connect(); // "localhost:80"
Closures
Die innere Funktion behält eine Referenz auf die Variablen der äußeren, keine Kopie. So lässt sich privater Zustand ohne Klassen umsetzen:
const createCounter = (start = 0) => {
let count = start;
return {
inc: () => ++count,
reset: () => { count = start; },
};
};
const counter = createCounter();
counter.inc(); counter.inc(); // 2
// count ist von außen nicht erreichbar
Closures sind auch die Grundlage von Callbacks, Event-Handlern, Memoisierung und Currying.
Fallstricke
varin Schleifen: Alle Closures teilen sich dieselbe Variable. Mitletbekommt jeder Durchlauf eine eigene Bindung.- Arrow Functions als Methoden:
thiszeigt dann nicht auf das Objekt. Nutze dafür die Methodenkurzschreibweiseobj = { run() { … } }. - Speicher: Closures halten referenzierte Objekte am Leben. Entferne Event-Listener, die du nicht mehr brauchst.
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 3 3 3
for (let j = 0; j < 3; j++) setTimeout(() => console.log(j)); // 0 1 2Arrays & Iterables
↗ Eigene SeiteDiese Sektion erklärt, wie du Arrays erzeugst, transformierst und destrukturierst und wie das Iterator-Protokoll funktioniert, auf dem for…of, Spread und Co. aufbauen.
Arrays sind geordnete Listen beliebiger Werte. Iterables sind das allgemeinere Konzept dahinter: Alles, was Symbol.iterator implementiert, also Arrays, Strings, Map und Set, kannst du mit for…of, Spread und Destrukturierung verarbeiten.
Arrays erzeugen
const nums = [1, 2, 3];
const gerade = Array.from({ length: 3 }, (_, i) => i * 2); // [0, 2, 4]
const chars = Array.from('abc'); // ['a', 'b', 'c']
const eins = Array.of(7); // [7]
Fallstrick: new Array(7) erzeugt kein Array mit der Zahl 7, sondern ein Array mit 7 leeren Plätzen.
Transformieren statt mutieren
map, filter und reduce geben neue Werte zurück und verändern das Original nicht. Seit ES2023 gibt es außerdem nicht-mutierende Varianten: toSorted, toReversed, toSpliced und with.
const users = [{ name: 'Ada', age: 36 }, { name: 'Linus', age: 28 }];
const namen = users.filter(({ age }) => age > 30).map(({ name }) => name);
const summe = users.reduce((acc, u) => acc + u.age, 0);
const sortiert = users.toSorted((a, b) => a.age - b.age);
sort() verändert das Original und vergleicht ohne Vergleichsfunktion als Strings: [10, 9, 1].sort() ergibt [1, 10, 9]. Gib deshalb immer eine Vergleichsfunktion mit.
Spread und Destrukturierung
const [first, , third, ...rest] = [1, 2, 3, 4, 5];
const kopie = [...nums, 4]; // flache Kopie
const unique = [...new Set([1, 1, 2, 3])]; // [1, 2, 3]
const letztes = nums.at(-1); // 3
Das Iterator-Protokoll
Ein Objekt ist iterierbar, wenn [Symbol.iterator]() einen Iterator zurückgibt. Dessen next() liefert jeweils { value, done }. Am einfachsten baust du so etwas mit Generatoren.
const range = (start, end) => ({
*[Symbol.iterator]() {
for (let i = start; i < end; i++) yield i;
},
});
for (const n of range(1, 4)) console.log(n); // 1, 2, 3
const arr = [...range(0, 3)]; // [0, 1, 2]
Tipp: for…in durchläuft Schlüssel, auch geerbte. Für die Werte eines Arrays nimmst du for…of. Für asynchrone Quellen wie Streams gibt es for await…of.
Strings & Template Literals
↗ Eigene SeiteDiese Sektion zeigt die drei String-Schreibweisen in JavaScript, Interpolation und mehrzeilige Texte mit Template Literals, Tagged Templates und die wichtigsten String-Methoden.
Strings sind in JavaScript unveränderliche Zeichenfolgen in UTF-16. Du schreibst sie mit einfachen oder doppelten Anführungszeichen oder mit Backticks. Backticks erzeugen Template Literals. Damit kannst du Werte direkt in den Text einsetzen und über mehrere Zeilen schreiben. Sobald Werte in den String sollen, sind sie heute die erste Wahl.
Drei Schreibweisen
const a = 'einfach';
const b = "doppelt";
const c = `Backtick`;
const pfad = 'C:\\temp\\datei.txt'; // Backslash escapen
' und " funktionieren genau gleich. Nutze im ganzen Projekt eine Variante und lass sie von Prettier oder ESLint durchsetzen.
Interpolation und Mehrzeiligkeit
In ${…} darf jeder beliebige Ausdruck stehen. Sein Ergebnis wird in einen String umgewandelt.
const user = { name: 'Lea', punkte: 42 };
const text = `${user.name} hat ${user.punkte * 2} Punkte.`;
const liste = `
<ul>
${['A', 'B'].map((x) => `<li>${x}</li>`).join('')}
</ul>`;
Fallstricke: Einrückungen und Zeilenumbrüche stehen genau so im String, wie du sie tippst. Objekte werden zu [object Object], dafür brauchst du JSON.stringify(). Setz Nutzereingaben nie ungeprüft in HTML ein, sonst droht XSS.
Tagged Templates
Steht eine Funktion direkt vor dem Backtick, bekommt sie die festen Textteile und die eingesetzten Werte getrennt übergeben. Das ist nützlich für Escaping, Übersetzungen (i18n) oder SQL-Builder.
const safe = (strings, ...values) =>
strings.reduce((out, str, i) => {
const v = String(values[i - 1] ?? '')
.replace(/[<>&]/g, (ch) => `&#${ch.charCodeAt(0)};`);
return out + v + str;
});
const eingabe = '<img onerror=alert(1)>';
safe`Hallo ${eingabe}!`; // 'Hallo <img onerror=alert(1)>!'
String.raw`C:\temp\neu`; // Backslashes bleiben erhalten
Wichtige Methoden
const s = ' JavaScript ';
s.trim(); // 'JavaScript'
s.includes('Script'); // true
'abc'.at(-1); // 'c'
'7'.padStart(3, '0'); // '007'
'a-b-c'.replaceAll('-', '/'); // 'a/b/c'
'a,b'.split(','); // ['a', 'b']
Jede dieser Methoden gibt einen neuen String zurück, das Original bleibt unverändert. length zählt UTF-16-Einheiten, deshalb zählen viele Emojis doppelt. Die tatsächlich sichtbaren Zeichen zählst du mit [...str].length.
Objekte & Prototypen
↗ Eigene SeiteDiese Sektion erklärt, wie Objekte in JavaScript aufgebaut sind, wie die Prototypenkette funktioniert und warum `class` nur eine lesbarere Schreibweise für Prototypen ist.
Objekte sind in JavaScript Sammlungen von Schlüssel-Wert-Paaren. Vererbung funktioniert über Prototypen: Jedes Objekt verweist intern ([[Prototype]]) auf ein anderes Objekt und erbt dessen Eigenschaften. Wer das Prinzip kennt, versteht auch class, this und die Suche nach Methoden.
Objekt-Literale
const user = {
name: 'Ada',
age: 36,
greet() {
return `Hallo, ${this.name}`;
},
};
const { name, ...rest } = user;
const copy = { ...user, age: 37 };
Der Spread-Operator erstellt nur eine flache Kopie. Für tiefe Kopien gibt es structuredClone().
Die Prototypenkette
Fehlt eine Eigenschaft am Objekt selbst, sucht die Engine im Prototyp weiter. Das geht so lange, bis sie null erreicht.
const animal = {
speak() { return `${this.name} macht ein Geräusch`; },
};
const dog = Object.create(animal);
dog.name = 'Rex';
dog.speak(); // 'Rex macht ein Geräusch'
Object.getPrototypeOf(dog) === animal; // true
Object.hasOwn(dog, 'speak'); // false
__proto__ ist veraltet. Nutze stattdessen Object.getPrototypeOf() und Object.setPrototypeOf(). Den Prototyp zur Laufzeit zu ändern macht den Code allerdings langsam.
Klassen als Syntax-Zucker
class baut dieselbe Prototypenstruktur auf, nur lesbarer. Methoden landen auf Klasse.prototype.
class Animal {
#id = crypto.randomUUID(); // privates Feld
constructor(name) { this.name = name; }
speak() { return `${this.name} macht ein Geräusch`; }
}
class Dog extends Animal {
speak() { return `${super.speak()} – Wuff!`; }
}
const rex = new Dog('Rex');
rex instanceof Animal; // true
Object.getPrototypeOf(Dog.prototype) === Animal.prototype; // true
Fallstricke
for...indurchläuft auch geerbte Eigenschaften. Besser sindObject.keys()oderObject.entries().- Arrow Functions als Methoden haben kein eigenes
this. Object.create(null)erzeugt ein Objekt ohne Prototyp. Das eignet sich als reines Dictionary. Oft ist aberMapdie bessere Wahl.
Klassen (ES6+)
↗ Eigene SeiteDiese Sektion zeigt die Klassensyntax ab ES2015: Konstruktor, Methoden, Getter, statische und private Member, Vererbung mit extends/super sowie typische Fallstricke rund um this.
Seit ES2015 definiert man Objekte mit gemeinsamem Verhalten meist über Klassen. Intern arbeitet JavaScript weiterhin mit Prototypen. class ist eine klarere und strengere Schreibweise für Konstruktorfunktionen.
Grundaufbau
class User {
role = 'gast'; // öffentliches Feld
#passwort; // privates Feld
constructor(name, passwort) {
this.name = name;
this.#passwort = passwort;
}
get anzeigeName() {
return `${this.name} (${this.role})`;
}
pruefe(eingabe) {
return eingabe === this.#passwort;
}
static gast() {
return new User('Gast', '');
}
}
const u = new User('Ada', 'geheim');
u.anzeigeName; // 'Ada (gast)'
u.pruefe('geheim'); // true
Methoden liegen auf User.prototype, Felder auf jeder Instanz. Auf #-Felder kann man von außen tatsächlich nicht zugreifen. Ein Versuch führt schon beim Parsen zu einem SyntaxError.
Vererbung mit extends und super
class Admin extends User {
role = 'admin';
constructor(name, passwort, rechte = []) {
super(name, passwort); // vor jedem this-Zugriff
this.rechte = rechte;
}
get anzeigeName() {
return `★ ${super.anzeigeName}`;
}
}
new Admin('Linus', 'root') instanceof User; // true
In abgeleiteten Klassen muss super() aufgerufen werden, bevor this verwendet wird. Sonst gibt es einen ReferenceError. Auch eingebaute Typen wie Error oder Array lassen sich erweitern.
this in Callbacks
Methoden sind nicht automatisch an ihr Objekt gebunden. Übergibt man eine Methode als Callback, geht this verloren. Abhilfe schafft eine Arrow-Function als Klassenfeld:
class Zaehler {
wert = 0;
erhoehe = () => { this.wert++; };
}
const z = new Zaehler();
button.addEventListener('click', z.erhoehe);
Der Nachteil: Arrow-Felder werden für jede Instanz neu erzeugt und liegen nicht auf dem Prototyp.
Fallstricke
- Klassen werden nicht wie Funktionen gehoistet. Wer sie vor der Deklaration nutzt, bekommt einen
ReferenceError(TDZ). - Der Klassenkörper läuft immer im Strict Mode.
- Ein Aufruf ohne
newwirft einenTypeError. - Private Member und
static-Blöcke gibt es seit ES2022. Alle aktuellen Browser unterstützen sie.
Module (import/export)
↗ Eigene SeiteES-Module teilen JavaScript-Code in Dateien mit eigenem Gültigkeitsbereich auf. Diese Sektion behandelt benannte und Default-Exporte, Importe, Re-Exporte, dynamisches import() und den Einsatz im Browser und in Node.js.
ES-Module (ESM) teilen Code in Dateien mit eigenem Gültigkeitsbereich auf. Was eine Datei nicht ausdrücklich exportiert, bleibt privat. Module laufen automatisch im Strict Mode und werden nur einmal ausgewertet, egal wie oft man sie importiert.
Benannte und Default-Exporte
Eine Datei kann beliebig viele benannte Exporte haben, aber höchstens einen Default-Export.
// math.js
export const PI = 3.14159;
export const add = (a, b) => a + b;
const secret = 42; // bleibt privat
export default function multiply(a, b) {
return a * b;
}
Importieren
import multiply, { PI, add as sum } from './math.js';
import * as math from './math.js';
console.log(sum(2, 3)); // 5
console.log(math.PI); // 3.14159
console.log(multiply(2, 4)); // 8
Importe sind Live-Bindings: Ändert das Quellmodul einen exportierten let-Wert, sieht das importierende Modul den neuen Wert. Selbst neu zuweisen kann es ihn aber nicht. Import-Deklarationen werden gehoistet und müssen auf oberster Ebene stehen.
Re-Exporte
So bündelt eine „Barrel“-Datei mehrere Module zu einer gemeinsamen Schnittstelle:
// index.js
export { add, PI } from './math.js';
export { default as multiply } from './math.js';
export * from './strings.js'; // ohne Default-Export!
Dynamischer Import
import() ist ein Ausdruck und hat als Rückgabewert ein Promise. Das eignet sich für Code-Splitting oder für Module, die nur unter bestimmten Bedingungen geladen werden sollen.
const loadChart = async () => {
const { default: Chart } = await import('./chart.js');
return new Chart('#canvas');
};
Im Browser und in Node.js
- Browser: Mit
<script type="module" src="app.js"></script>wird das Skript automatisch verzögert (deferred) geladen. - Node.js: Dateiendung
.mjsverwenden oder"type": "module"in diepackage.jsoneintragen. - Im Browser brauchen Pfade die Dateiendung. Paketnamen ohne Pfad wie
'lodash'funktionieren nur mit einer Import Map oder einem Bundler. - Top-Level-
awaitist nur in Modulen erlaubt.
Tipp: Benannte Exporte sind meist die bessere Wahl. Sie machen Refactoring, Autovervollständigung und Tree-Shaking einfacher.
Asynchronität (Promises, async/await)
↗ Eigene SeiteDiese Sektion erklärt, wie JavaScript mit Promises und async/await asynchron arbeitet. Behandelt werden Verkettung, Fehlerbehandlung, parallele Ausführung und typische Fallstricke.
JavaScript läuft in einem einzigen Thread. Netzwerkzugriffe und Timer blockieren ihn trotzdem nicht, weil ihre Ergebnisse später über die Event Loop geliefert werden. Ein Promise steht für einen solchen zukünftigen Wert. async/await ist die gut lesbare Syntax, mit der du Promises verarbeitest.
Promises
Ein Promise hat einen von drei Zuständen: pending (ausstehend), fulfilled (erfüllt) oder rejected (abgelehnt). Mit then, catch und finally reagierst du auf das Ergebnis. Jeder dieser Aufrufe gibt ein neues Promise zurück, deshalb kannst du sie verketten.
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
wait(500)
.then(() => fetch('/api/user'))
.then((res) => res.json())
.catch((err) => console.error(err))
.finally(() => console.log('fertig'));
async/await
Eine async-Funktion gibt immer ein Promise zurück. await hält die Funktion an, bis das Promise erfüllt ist, und liefert dann dessen Wert. Wird das Promise abgelehnt, wirft await eine Ausnahme, die du mit try/catch abfangen kannst.
const loadUser = async (id) => {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { name } = await res.json();
return name;
} catch (err) {
console.error('Laden fehlgeschlagen:', err);
throw err;
}
};
Achtung: fetch lehnt bei HTTP-Fehlern wie 404 nicht ab, deshalb musst du res.ok selbst prüfen. await auf oberster Ebene (Top-Level-Await) funktioniert nur in ES-Modulen.
Parallel statt nacheinander
Wenn du unabhängige Aufgaben nacheinander mit await abwartest, verlierst du Zeit. Starte sie stattdessen gemeinsam:
const [user, posts] = await Promise.all([
fetch('/api/user').then((r) => r.json()),
fetch('/api/posts').then((r) => r.json()),
]);
Promise.all: wird beim ersten Fehler abgelehnt.Promise.allSettled: wartet auf alle Promises und liefert für jedes Status und Wert bzw. Fehlergrund.Promise.race/Promise.any: liefert das erste abgeschlossene bzw. das erste erfolgreiche Ergebnis.
Fallstricke
awaitinnerhalb vonforEachwartet nicht. Nutzefor...ofoderPromise.allmitmap.- Ohne
awaitbekommst du ein Promise statt des eigentlichen Werts. - Unbehandelte Ablehnungen lösen
unhandledrejectionaus. Fange Fehler deshalb immer mitcatchodertry/catchab.
Fehlerbehandlung & Exceptions
↗ Eigene SeiteDiese Sektion zeigt, wie du in JavaScript Fehler mit try/catch/finally abfängst, mit throw eigene Exceptions auslöst und Fehler in asynchronem Code sauber behandelst.
Laufzeitfehler brechen die Ausführung ab, solange sie niemand abfängt. Mit try, catch und finally fängst du sie gezielt ab. Mit throw meldest du selbst Fehler. Das brauchst du überall, wo Eingaben, Netzwerk oder Parsing schiefgehen können.
try, catch und finally
Der catch-Block läuft nur, wenn im try-Block ein Fehler auftritt. finally läuft immer, auch nach einem return. Seit ES2019 darfst du den Parameter weglassen (optional catch binding).
const parseConfig = (json) => {
try {
return JSON.parse(json);
} catch (err) {
console.error(`Ungültiges JSON: ${err.message}`);
return {};
} finally {
console.log('Parsing abgeschlossen');
}
};
try { riskyCall(); } catch { /* Fehler bewusst ignoriert */ }
Fehler werfen und eigene Fehlerklassen
Wirf immer Error-Objekte oder Unterklassen davon, keine Strings. Nur so bekommst du message, name und einen Stacktrace. Eingebaute Typen sind z. B. TypeError, RangeError und SyntaxError. Mit der Option cause (ES2022) hängst du den ursprünglichen Fehler an.
class ValidationError extends Error {
constructor(field, message, options) {
super(message, options);
this.name = 'ValidationError';
this.field = field;
}
}
try {
throw new ValidationError('email', 'E-Mail fehlt');
} catch (err) {
if (err instanceof ValidationError) {
console.warn(`${err.field}: ${err.message}`);
} else {
throw err; // Unbekanntes nicht verschlucken
}
}
Fehler in asynchronem Code
Bei await funktioniert try/catch wie bei synchronem Code. Aufgepasst: fetch wirft bei HTTP-Status 404 oder 500 keinen Fehler. Den Status musst du selbst prüfen.
const loadUser = async (id) => {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
throw new Error('Benutzer konnte nicht geladen werden', { cause: err });
}
};
loadUser(42).catch((err) => console.error(err.message, err.cause));
Best Practices
- Fang nur Fehler ab, die du auch sinnvoll behandeln kannst. Alles andere wirfst du erneut.
- Promises ohne
awaitoder.catch()führen zu unhandled rejections. - Ein
returninfinallyüberschreibt Rückgabewerte und verschluckt Fehler. Das solltest du vermeiden.
DOM & Web-APIs
↗ Eigene SeiteDiese Sektion zeigt, wie du mit JavaScript im Browser das DOM ausliest und veränderst, auf Events reagierst und mit fetch Daten nachlädst.
Im Browser verbindet JavaScript deinen Code mit der Seite. Über das DOM (Document Object Model) liest und änderst du HTML-Elemente. Über Web-APIs wie fetch oder localStorage greifst du auf Netzwerk und Speicher zu. Diese APIs gehören nicht zur Sprache selbst. Sie kommen von der Laufzeitumgebung, deshalb fehlen sie in Node.js teilweise.
Elemente finden und verändern
querySelector gibt das erste passende Element zurück oder null, wenn nichts passt. querySelectorAll gibt eine statische NodeList zurück. Für reinen Text nimmst du besser textContent als innerHTML. So verhinderst du XSS-Lücken.
const list = document.querySelector('#todos');
const items = document.querySelectorAll('.todo');
items.forEach((el) => el.classList.toggle('done'));
const li = document.createElement('li');
li.textContent = 'Neue Aufgabe';
li.dataset.id = '42';
list?.append(li);
Events behandeln
Mit addEventListener registrierst du Handler. Bei Event Delegation genügt ein einziger Listener am Elternelement. Er greift auch für Kinder, die erst später hinzukommen. Mit einem AbortController kannst du mehrere Listener auf einmal entfernen.
const controller = new AbortController();
list.addEventListener('click', (event) => {
const item = event.target.closest('.todo');
if (!item) return;
console.log(`Klick auf ${item.dataset.id}`);
}, { signal: controller.signal });
// später: alle Listener mit diesem Signal entfernen
controller.abort();
Daten laden mit fetch
fetch gibt ein Promise zurück. Achtung: HTTP-Fehler wie 404 oder 500 lassen das Promise nicht scheitern. Es wird nur bei Netzwerkfehlern abgelehnt. Prüfe deshalb response.ok selbst.
const loadUser = async (id) => {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { name, email } = await res.json();
return { name, email };
};
Praxistipps
- Binde Skripte mit
deferodertype="module"ein. Dann ist das DOM schon fertig aufgebaut, wenn dein Code läuft. localStoragespeichert nur Strings und arbeitet synchron. Objekte wandelst du vorher mitJSON.stringifyum.
Best Practices & Fallstricke
↗ Eigene SeiteDie häufigsten Fallstricke in JavaScript und wie du sie vermeidest: Vergleiche, Standardwerte, Mutation, asynchroner Code und Gleitkommazahlen.
Viele Bugs in JavaScript entstehen nicht durch komplexe Logik, sondern durch bekannte Eigenheiten der Sprache. Hier findest du die häufigsten Stolperfallen und wie du sie mit moderner Syntax umgehst.
Strikter Vergleich und sinnvolle Standardwerte
Nutze === statt ==, dann gibt es keine implizite Typumwandlung. Für Standardwerte ist ?? meist besser als ||, weil es 0 und '' stehen lässt.
0 == ''; // true – Überraschung
0 === ''; // false
const limit = options.limit || 10; // 0 wird zu 10!
const safeLimit = options.limit ?? 10; // 0 bleibt 0
const city = user?.address?.city ?? 'unbekannt';
Ungewollte Mutation vermeiden
const schützt nur die Bindung, nicht den Inhalt. sort(), reverse() und splice() verändern das Original-Array. Seit ES2023 gibt es Varianten, die das Original unverändert lassen.
const nums = [10, 1, 5];
const sorted = nums.toSorted((a, b) => a - b); // nums bleibt gleich
const copy = { ...config, debug: true }; // flache Kopie
const deep = structuredClone(config); // tiefe Kopie
Achtung: sort() ohne Vergleichsfunktion sortiert als Zeichenketten. [10, 1, 5].sort() ergibt [1, 10, 5].
Asynchronen Code richtig schreiben
forEach wartet nicht auf async-Callbacks. Nimm for...of, wenn die Schritte nacheinander laufen sollen, und Promise.all, wenn sie parallel laufen dürfen. Fang Fehler immer ab.
const loadAll = async (ids) => {
try {
return await Promise.all(ids.map(async (id) => {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}));
} catch (err) {
console.error('Laden fehlgeschlagen:', err);
return [];
}
};
Wichtig: fetch wirft bei 404 oder 500 keinen Fehler. Prüf deshalb immer res.ok.
Zahlen und Gleitkomma
Zahlen in JavaScript sind binäre Gleitkommazahlen (IEEE 754). Vergleich sie deshalb mit einer Toleranz und speicher Geldbeträge als ganze Cent-Werte.
0.1 + 0.2 === 0.3; // false
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON; // true
Number.isNaN('abc'); // false (isNaN('abc') → true)Code-Snippets
Kopierfertige Rezepte für den Alltag — mit Erklärung und erwartetem Output.
HTTP & Web
fetch-Wrapper mit Timeout pro Versuch und automatischen Wiederholungen mit exponentiellem Backoff bei Netzwerkfehlern, 429 und 5xx. Für instabile APIs.
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function fetchWithRetry(url, { retries = 3, timeout = 5000, backoff = 500, ...opts } = {}) {
for (let attempt = 0; ; attempt++) {
try {
// Eigener Timeout pro Versuch
const res = await fetch(url, { ...opts, signal: AbortSignal.timeout(timeout) });
// Nur 429 und 5xx wiederholen, 4xx direkt zurückgeben
if ((res.status === 429 || res.status >= 500) && attempt < retries) {
throw new Error(`HTTP ${res.status}`);
}
return res;
} catch (err) {
if (attempt >= retries) throw err;
// Exponentieller Backoff mit Jitter
await sleep(backoff * 2 ** attempt + Math.random() * 100);
}
}
}
// in async-Funktion:
// const res = await fetchWithRetry('/api/data', { timeout: 3000, retries: 2 });
JSON & Serialisierung
Parst JSON ohne Exception mit Fallback-Wert und entfernt __proto__-Schlüssel. Serialisiert auch zirkuläre Objekte und BigInt, ohne abzustürzen.
// Parsen mit Fallback, entfernt __proto__/constructor (Schutz vor Prototype Pollution)
const safeParse = (text, fallback = null) => {
if (typeof text !== 'string') return fallback;
try {
return JSON.parse(text, (k, v) =>
k === '__proto__' || k === 'constructor' ? undefined : v);
} catch {
return fallback;
}
};
// Serialisieren ohne TypeError bei Zyklen oder BigInt
const safeStringify = (value, space) => {
const seen = new WeakSet(); // markiert auch mehrfach genutzte Referenzen
try {
return JSON.stringify(value, (k, v) => {
if (typeof v === 'bigint') return v.toString();
if (v && typeof v === 'object') {
if (seen.has(v)) return '[Circular]';
seen.add(v);
}
return v;
}, space);
} catch {
return undefined;
}
};
const obj = { id: 1n };
obj.self = obj;
console.log(safeStringify(obj));
console.log(safeParse('kaputt', {}));
Ein- & Ausgabe
Liest und schreibt Textdateien asynchron mit fs/promises. Das Schreiben ist atomar: Erst wird eine temporäre Datei geschrieben, dann umbenannt, damit bei einem Absturz keine halb geschriebene Datei übrig bleibt.
import { readFile, writeFile, rename, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
// Datei lesen – gibt null zurück, wenn sie nicht existiert
export const readText = async (path) => {
try {
return await readFile(path, 'utf8');
} catch (err) {
if (err.code === 'ENOENT') return null;
throw err;
}
};
// Atomar schreiben: erst in Temp-Datei, dann umbenennen
export const writeText = async (path, data) => {
await mkdir(dirname(path), { recursive: true });
const tmp = `${path}.${process.pid}.tmp`;
await writeFile(tmp, data, 'utf8');
await rename(tmp, path);
};
// Verwendung (ESM, top-level await)
await writeText('./data/notes.txt', 'Erste Zeile\n');
const content = await readText('./data/notes.txt');
console.log(content);
Datum & Zeit
Formatiert ein Datum mit Intl.DateTimeFormat passend zu Sprache und Zeitzone. Formatter werden zwischengespeichert, ungültige Datumswerte lösen einen Fehler aus.
// Formatter-Erzeugung ist teuer -> pro Locale+Optionen cachen
const cache = new Map();
const formatDate = (value, { locale = 'de-DE', ...opts } = {}) => {
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) {
throw new RangeError(`Ungültiges Datum: ${value}`);
}
const key = `${locale}|${JSON.stringify(opts)}`;
let fmt = cache.get(key);
if (!fmt) {
fmt = new Intl.DateTimeFormat(locale, opts);
cache.set(key, fmt);
}
return fmt.format(date);
};
const d = '2024-03-15T14:30:00Z';
console.log(formatDate(d, { dateStyle: 'long', timeZone: 'Europe/Berlin' }));
console.log(formatDate(d, {
locale: 'en-US', dateStyle: 'full', timeStyle: 'short',
timeZone: 'America/New_York',
}));
Krypto & Passwörter
Hasht Passwörter mit PBKDF2-SHA256 (600k Iterationen, zufälliger Salt) über die Web Crypto API und prüft sie mit zeitkonstantem Vergleich. Funktioniert im Browser und in Node.js ab Version 20.
const b64 = (b) => btoa(String.fromCharCode(...new Uint8Array(b)));
const unb64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
const derive = async (pw, salt, iterations) => {
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(pw), 'PBKDF2', false, ['deriveBits']);
return crypto.subtle.deriveBits({ name: 'PBKDF2', hash: 'SHA-256', salt, iterations }, key, 256);
};
// Format: iter$salt$hash
const hashPassword = async (pw, iter = 600_000) => {
const salt = crypto.getRandomValues(new Uint8Array(16));
return `${iter}$${b64(salt)}$${b64(await derive(pw, salt, iter))}`;
};
const verifyPassword = async (pw, stored) => {
const [iter, salt, hash] = stored.split('$');
const a = new Uint8Array(await derive(pw, unb64(salt), +iter));
const b = unb64(hash);
// zeitkonstanter Vergleich
return a.length === b.length && a.reduce((d, x, i) => d | (x ^ b[i]), 0) === 0;
};
Erzeugt einen kryptografisch sicheren, URL-tauglichen Zufallstoken (Base64url) für Session-IDs, Reset-Links oder API-Keys. Läuft im Browser und in Node.js ab Version 19.
// Browser + Node.js ≥ 19 (globalThis.crypto = Web Crypto API)
const randomToken = (bytes = 32) => {
// Unter 128 Bit Entropie ist ein Token leichter zu erraten
if (!Number.isInteger(bytes) || bytes < 16 || bytes > 65536) {
throw new RangeError('bytes muss eine ganze Zahl zwischen 16 und 65536 sein');
}
const buf = crypto.getRandomValues(new Uint8Array(bytes));
// Base64url: sicher für URLs und Cookies, ohne Padding
let bin = '';
for (const b of buf) bin += String.fromCharCode(b);
return btoa(bin)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
};
console.log(randomToken()); // 256 Bit
console.log(randomToken(16)); // 128 Bit
Reguläre Ausdrücke
Treffer samt benannten Gruppen und Position per matchAll auslesen, per Callback ersetzen und Benutzereingaben sicher escapen, bevor man sie als RegExp nutzt.
// TT.MM.JJJJ mit benannten Gruppen
const DATE_RE = /(?<d>\d{2})\.(?<m>\d{2})\.(?<y>\d{4})/g;
// Alle Treffer mit Gruppen und Index
const findDates = (text) =>
[...text.matchAll(DATE_RE)].map(({ groups, index }) => ({ ...groups, index }));
// Ersetzen per Callback: TT.MM.JJJJ -> JJJJ-MM-TT
const toIsoDates = (text) =>
text.replace(DATE_RE, (_, d, m, y) => `${y}-${m}-${d}`);
// Nutzereingabe als Literal suchen (Sonderzeichen escapen)
const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Callback verhindert, dass $& / $1 im Ersatztext interpretiert werden
const replaceLiteral = (text, search, repl) =>
text.replace(new RegExp(escapeRegex(search), 'gi'), () => repl);
const txt = 'Start 01.03.2024, Ende 15.04.2024 (Preis: 9.99$)';
console.log(findDates(txt));
console.log(toIsoDates(txt));
console.log(replaceLiteral(txt, '9.99$', '12,50 €'));
Umgebung & Konfig
Lädt Env-Variablen typsicher und prüft sie beim Start: Pflichtwerte, Zahlen, Booleans. Fehlt eine Variable, bricht die App sofort ab. Dazu der Zugriff in Vite über import.meta.env.
// Node.js ≥ 20.6: node --env-file=.env app.js (kein dotenv nötig)
const required = (key) => {
const val = process.env[key];
if (val === undefined || val === '') throw new Error(`Env-Variable fehlt: ${key}`);
return val;
};
const toInt = (key, fallback) => {
const n = Number.parseInt(process.env[key] ?? String(fallback), 10);
if (Number.isNaN(n)) throw new Error(`${key} ist keine Zahl`);
return n;
};
// Beim Start einmal prüfen, danach nur noch config nutzen
export const config = Object.freeze({
nodeEnv: process.env.NODE_ENV ?? 'development',
port: toInt('PORT', 3000),
dbUrl: required('DATABASE_URL'),
debug: process.env.DEBUG === 'true',
});
// Vite (Browser): nur Variablen mit VITE_-Präfix landen im Bundle!
// const apiUrl = import.meta.env.VITE_API_URL;
// if (import.meta.env.DEV) console.log('Dev-Modus');
Strings & Regex
Macht aus einem beliebigen String einen URL-tauglichen Slug. Umlaute werden deutsch transkribiert (ä→ae, ß→ss), Akzente entfernt, Sonderzeichen ersetzt. Optional mit Maximallänge und eigenem Trennzeichen.
const UMLAUTE = { ä: 'ae', ö: 'oe', ü: 'ue', ß: 'ss', Ä: 'Ae', Ö: 'Oe', Ü: 'Ue', ẞ: 'SS' };
const slugify = (input, { maxLength = 80, separator = '-' } = {}) =>
String(input ?? '')
// Deutsche Umlaute zuerst ersetzen, sonst würde NFD aus ä nur ein a machen
.replace(/[äöüßÄÖÜẞ]/g, (c) => UMLAUTE[c])
// Übrige Akzente abtrennen und entfernen (é → e, ç → c)
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
// Alles außer a-z/0-9 wird zu einem einzelnen Bindestrich
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, maxLength)
// Nach dem Kürzen keinen Bindestrich am Ende stehen lassen
.replace(/-+$/, '')
.replaceAll('-', separator);
console.log(slugify('Größenänderung für Übergrößen!'));
console.log(slugify(' Café & Crème brûlée '));
console.log(slugify('Node.js 20: Was ist neu?', { separator: '_' }));
Sonstiges
Debounce führt eine Funktion erst aus, wenn die Aufrufe eine Weile pausieren (z. B. Suche, Resize). Throttle führt sie höchstens einmal pro Intervall aus (z. B. Scroll), der letzte Aufruf geht nicht verloren.
// Debounce: erst ausführen, wenn seit dem letzten Aufruf `wait` ms vergangen sind
const debounce = (fn, wait = 200) => {
let t;
const d = function (...args) {
clearTimeout(t);
t = setTimeout(() => fn.apply(this, args), wait);
};
d.cancel = () => clearTimeout(t); // z. B. beim Unmount aufrufen
return d;
};
// Throttle: höchstens 1× pro `wait` ms, der letzte Aufruf wird nachgeholt
const throttle = (fn, wait = 200) => {
let last = 0, t;
const th = function (...args) {
const run = () => { last = Date.now(); fn.apply(this, args); };
clearTimeout(t);
const rest = wait - (Date.now() - last);
rest <= 0 ? run() : (t = setTimeout(run, rest));
};
th.cancel = () => clearTimeout(t);
return th;
};
// Verwendung
input.addEventListener('input', debounce(e => search(e.target.value), 300));
window.addEventListener('scroll', throttle(() => updateHeader(scrollY), 100));
Referenz-Bibliothek
Jede Funktion, Klasse und jedes Sprach-Konstrukt lebt auf einer eigenen Seite — mit Signatur, Parameter-Tabelle, Beispielen und Fallstricken.