Modernes JavaScript: Sprachkonzepte und Werkzeugkette im Überblick

Module, asynchroner Code, Laufzeitumgebungen, Bundler und Typprüfung: wie die Bausteine eines heutigen JavaScript-Projekts zusammenpassen
Aktualisiert:
Abstract
- #JavaScript
- #TypeScript
- #Webentwicklung
- #Programmierung
- #Node.js
- #ES-Module
- #Vite
- #Build-Tools
Was ein JavaScript-Projekt heute wirklich braucht und welche Werkzeuge Sie getrost weglassen können
Wer heute ein neues JavaScript-Projekt anlegt, steht vor einer langen Liste von Entscheidungen: Node.js, Deno oder Bun? Vite oder Webpack? TypeScript oder reines JavaScript? ESLint, Prettier oder Biome? Für Einsteiger wirkt das wie ein Dschungel, und selbst erfahrene Teams fragen sich, welche Werkzeuge aus alten Projekten noch nötig sind.
Die gute Nachricht: Die Sprache selbst und ihre Werkzeugkette sind in den letzten Jahren deutlich aufgeräumter geworden. Vieles, wofür man früher eigene Pakete brauchte, steckt inzwischen in der Sprache oder in der Laufzeitumgebung. Dieser Überblick ordnet die Bausteine eines modernen JavaScript-Projekts: die Sprachkonzepte, die den Alltag prägen, und die Werkzeuge, die daraus ein lauffähiges Produkt machen. Einzelne Neuerungen einer Sprachversion stehen hier nicht im Mittelpunkt, sondern das Zusammenspiel.
Die Sprache: was modernes JavaScript ausmacht
JavaScript wird unter dem Namen ECMAScript von einem Gremium standardisiert, dem TC39. Seit 2015 erscheint jedes Jahr eine neue Version, etwa ES2024 oder ES2025. Die großen Umbrüche liegen dabei schon einige Jahre zurück. Die Version ES2015, oft ES6 genannt, brachte let und const, Pfeilfunktionen, Klassen, Module und Promises. Seitdem kommen jährlich kleinere Ergänzungen hinzu. Wer heute „modernes JavaScript“ sagt, meint vor allem einen Stil, der diese Bausteine konsequent nutzt. Ob eine neuere Funktion in allen Browsern verfügbar ist, die Ihre Nutzer verwenden, zeigt die Kennzeichnung „Baseline“ in der Entwicklerdokumentation MDN: Sie markiert Funktionen, die in den aktuellen Versionen aller großen Browser unterstützt werden. Für ältere Zielumgebungen übersetzt der Build-Schritt neuere Syntax bei Bedarf in ältere.
Einige Konzepte begegnen Ihnen in fast jeder Codebasis:
const kunde = { name: 'Schmitz', adresse: { ort: 'Köln' } };
// Pfeilfunktion mit Standardwert
const gruss = (name = 'Gast') => `Guten Tag, ${name}`;
// Destructuring und Spread
const { name, ...rest } = kunde;
const aktualisiert = { ...kunde, telefon: '0221 123456' };
// Optionale Verkettung und Ersatzwert
const plz = kunde.adresse?.plz ?? 'unbekannt';
console.log(gruss(name), rest, aktualisiert.telefon, plz);
Pfeilfunktionen sind nicht nur kürzer. Sie übernehmen den Wert von this aus ihrer Umgebung, statt einen eigenen zu bilden, was in Callbacks und Event-Handlern viele frühere Fehler vermeidet. Destructuring zerlegt Objekte und Arrays in einzelne Variablen, der Spread-Operator ... setzt sie zu neuen zusammen. Die optionale Verkettung ?. bricht ab, wenn ein Zwischenwert fehlt, und ?? liefert dann einen Ersatzwert. Wichtig dabei: Spread kopiert nur die oberste Ebene. Verschachtelte Objekte wie adresse teilen sich Original und Kopie weiterhin, weil Objekte in JavaScript als Referenz weitergegeben werden.
Klassen mit echter Kapselung
JavaScript basiert auf Prototypen: Objekte erben direkt von anderen Objekten. Die Klassensyntax ist eine lesbarere Schreibweise für dieses Prinzip. Seit ES2022 gibt es private Felder und Methoden, die mit # beginnen und von außen tatsächlich nicht erreichbar sind:
class Buch {
#provisionssatz = 0.12;
constructor(titel, seiten, isbn) {
this.titel = titel;
this.seiten = seiten;
this.isbn = isbn;
}
get provision() {
return this.seiten * this.#provisionssatz;
}
beschreibung() {
return `${this.titel} (ISBN ${this.isbn})`;
}
}
class EBook extends Buch {
constructor(titel, seiten, isbn, format) {
super(titel, seiten, isbn);
this.format = format;
}
beschreibung() {
return `${super.beschreibung()} als ${this.format}`;
}
}
const buch = new EBook('Modernes JavaScript', 320, '978-3-00-000000-0', 'EPUB');
console.log(buch.beschreibung());
console.log(buch.provision.toFixed(2)); // '38.40'
// buch.#provisionssatz wäre ein Syntaxfehler
Das Beispiel zeigt Kapselung durch das private Feld, einen berechneten Wert über einen Getter, Vererbung mit extends und das Überschreiben einer Methode, bei dem super die ursprüngliche Fassung aufruft. Klassen sind in JavaScript allerdings kein Muss. Viele Teams kommen mit einfachen Objekten und Funktionen aus und setzen Klassen nur dort ein, wo Zustand und Verhalten eng zusammengehören.
Module: import und export als Standard
Lange Zeit hatte JavaScript kein eigenes Modulsystem. Node.js nutzte CommonJS mit require(), im Browser luden Bundler und Skript-Tags den Code. Heute sind ES-Module, kurz ESM, der Standard in beiden Welten. Jede Datei ist ein eigenes Modul mit eigenem Gültigkeitsbereich und gibt nur frei, was sie ausdrücklich exportiert:
// preise.js
export const MWST = 0.19;
export function brutto(netto) {
return Math.round(netto * (1 + MWST) * 100) / 100;
}
// main.js
import { brutto } from './preise.js';
console.log(brutto(100)); // 119
In Node.js schalten Sie ESM mit "type": "module" in der package.json ein oder verwenden die Endung .mjs. Im Browser genügt type="module" am Skript-Tag. Module laufen automatisch im strikten Modus, und import-Anweisungen werden vor der Ausführung aufgelöst. Dadurch können Bundler ungenutzte Exporte erkennen und weglassen, was als Tree Shaking bezeichnet wird.
Für Bestandsprojekte ist der Wechsel von CommonJS zu ESM oft der aufwendigste Modernisierungsschritt, weil Pfade, Konfigurationsdateien und manche Abhängigkeiten angepasst werden müssen. Den Ablauf beschreibt der Beitrag Von CommonJS zu ESM: Node.js-Projekte auf import umstellen.
Asynchroner Code: Promises und async/await
JavaScript führt Code in einem einzigen Thread aus. Damit eine Anwendung während einer Netzwerkanfrage nicht einfriert, arbeitet die Sprache mit einer Ereignisschleife: Langsame Vorgänge werden angestoßen, und das Ergebnis wird verarbeitet, sobald es da ist. Früher geschah das mit verschachtelten Callbacks, heute mit Promises und den Schlüsselwörtern async und await:
async function ladeBestellungen(kundenId) {
const antwort = await fetch(
`https://api.example.com/kunden/${kundenId}/bestellungen`,
);
if (!antwort.ok) {
throw new Error(`Anfrage fehlgeschlagen: ${antwort.status}`);
}
return antwort.json();
}
try {
const [schmitz, albers] = await Promise.all([
ladeBestellungen(17),
ladeBestellungen(42),
]);
console.log(schmitz.length, albers.length);
} catch (fehler) {
console.error(fehler.message);
}
Der Code liest sich wie eine Abfolge von Schritten, obwohl er asynchron abläuft. Fehler fangen Sie mit dem gewohnten try/catch ab. Promise.all() startet beide Anfragen parallel und wartet, bis beide fertig sind. Das await außerhalb einer Funktion, das sogenannte Top-Level-Await, funktioniert nur in ES-Modulen. Die Funktion fetch() ist im Browser seit Langem vorhanden und gehört inzwischen auch in Node.js, Deno und Bun zur Grundausstattung, sodass dafür kein Zusatzpaket mehr nötig ist.
Die häufigste Falle in der Praxis ist ein vergessenes await. Die Funktion liefert dann ein Promise statt des Ergebnisses, und Fehler gehen still verloren. Linter erkennen viele solcher Stellen. Eine ausführliche Einführung bietet der Beitrag Asynchrones JavaScript: Promises und async/await verständlich erklärt.
Laufzeitumgebungen: Browser, Node.js, Deno und Bun
JavaScript braucht eine Laufzeitumgebung, die den Code ausführt und Schnittstellen zur Außenwelt bereitstellt. Im Browser sind das das DOM, also die Programmierschnittstelle der Webseite, sowie Funktionen wie fetch() und localStorage. Auf dem Server und auf dem Entwicklerrechner kommen drei Umgebungen infrage:
- Node.js ist der Standard, seit 2009 verfügbar und in nahezu allen Werkzeugen und Hosting-Angeboten unterstützt. Für den produktiven Einsatz empfehlen sich die Versionen mit Langzeitunterstützung (LTS). Node.js hat in den letzten Jahren viele Funktionen eingebaut, für die früher Pakete nötig waren, etwa einen Test-Runner,
fetch()und das Einlesen von.env-Dateien. - Deno stammt vom ursprünglichen Node.js-Entwickler Ryan Dahl. Es setzt auf Sicherheit durch Berechtigungen, die Sie beim Start ausdrücklich erteilen, etwa für Netzwerk- oder Dateizugriffe, und führt TypeScript direkt aus. Seit Version 2 ist Deno weitgehend mit npm-Paketen kompatibel.
- Bun kombiniert Laufzeit, Paketmanager, Bundler und Test-Runner in einem Programm und wirbt mit hoher Geschwindigkeit. Es ist weitgehend kompatibel zu Node.js, aber nicht in allen Details.
Unsere Einschätzung: Für Unternehmensprojekte bleibt Node.js die sichere Wahl, weil Hosting, Monitoring und Bibliotheken darauf ausgerichtet sind. Deno und Bun sind interessant für neue, überschaubare Projekte, interne Werkzeuge und Skripte. Ein Wechsel lohnt sich vor allem dann, wenn ein konkretes Problem wie lange Installationszeiten im Build gelöst wird, nicht als Selbstzweck.
Bundler und Build: vom Quelltext zur ausgelieferten Datei
Moderne Browser verstehen ES-Module direkt. Trotzdem durchläuft fast jede Webanwendung einen Build-Schritt. Ein Bundler fasst die vielen Module und Abhängigkeiten zu wenigen optimierten Dateien zusammen, entfernt ungenutzten Code, verkleinert ihn (Minifizierung) und teilt ihn so auf, dass der Browser nur lädt, was die aktuelle Seite braucht. Nebenbei übersetzt er TypeScript, JSX und CSS-Varianten in Code, den der Browser versteht.
Den Platzhirsch Webpack hat in neuen Projekten weitgehend Vite abgelöst. Vite startet den Entwicklungsserver, ohne den Quellcode vorher zu bündeln, liefert die Module einzeln an den Browser aus und aktualisiert nach einer Änderung nur das betroffene Modul. Für den Produktions-Build bündelt es dann klassisch. Die meisten Frameworks wie Vue, Svelte oder React-Vorlagen setzen auf Vite oder liefern einen eigenen, darauf aufbauenden Build mit. Unter der Haube arbeiten zunehmend Werkzeuge, die in Rust oder Go geschrieben sind, etwa esbuild, Rolldown oder SWC, was Build-Zeiten deutlich verkürzt.
Ein minimales Projekt mit Vite sieht in der package.json etwa so aus:
{
"name": "bestellportal",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"lint": "eslint ."
},
"devDependencies": {
"eslint": "^10.0.0",
"typescript": "^7.0.0",
"vite": "^8.0.0",
"vitest": "^5.0.0"
}
}
Für Node.js-Anwendungen auf dem Server ist ein Bundler dagegen oft überflüssig. Node.js lädt die Module direkt, und ein zusätzlicher Build-Schritt macht Fehlersuche und Deployment nur komplizierter. Wie Vite im Detail arbeitet und konfiguriert wird, erläutert der Beitrag Vite als Build-Tool.
Paketmanager und Abhängigkeiten
Fast jedes Projekt bezieht Bibliotheken aus der npm-Registry, dem zentralen Paketverzeichnis für JavaScript. Neben dem mitgelieferten npm sind pnpm und Yarn verbreitet. pnpm speichert jedes Paket nur einmal auf der Festplatte und verlinkt es in die Projekte, was Platz und Installationszeit spart. Unabhängig vom Werkzeug gehört die Sperrdatei, etwa package-lock.json oder pnpm-lock.yaml, ins Versionsverwaltungssystem. Sie hält die exakt installierten Versionen fest, sodass Entwicklerrechner und Build-Server denselben Stand verwenden.
Abhängigkeiten sind zugleich ein Sicherheitsthema. Jedes Paket bringt meist eigene Abhängigkeiten mit, sodass im Ordner node_modules oft ein Vielfaches der Pakete landet, die in der package.json stehen. Zudem tauchten in den vergangenen Jahren in der npm-Registry wiederholt manipulierte Pakete auf. Regelmäßige Prüfungen mit npm audit oder automatisierte Update-Werkzeuge in der Versionsverwaltung gehören deshalb zur Grundausstattung. Noch wirksamer ist es, Abhängigkeiten sparsam einzusetzen und vor jedem neuen Paket zu fragen, ob die Sprache oder die Laufzeit die Aufgabe nicht schon selbst erledigt.
Qualitätssicherung: Linter, Formatierer und Tests
Zur Werkzeugkette gehören drei weitere Bausteine. Ein Linter wie ESLint prüft den Code auf typische Fehler und Stilverstöße, etwa ungenutzte Variablen oder vergessene await. Ein Formatierer wie Prettier sorgt für einheitliche Einrückung und Zeilenumbrüche, sodass Code-Reviews sich auf den Inhalt konzentrieren. Biome bündelt beides in einem schnellen Werkzeug, deckt aber noch nicht jede ESLint-Regel ab. Für Tests sind Vitest im Frontend und der eingebaute Test-Runner node:test auf dem Server verbreitet. Wichtig ist weniger die Wahl des Werkzeugs als dass alle Prüfungen automatisch laufen, lokal vor dem Commit und in der CI-Pipeline.
Typprüfung: TypeScript und Alternativen
JavaScript ist dynamisch typisiert. Eine Variable kann erst eine Zahl und dann eine Zeichenkette enthalten, und ein Tippfehler im Eigenschaftsnamen fällt erst zur Laufzeit auf. TypeScript ergänzt JavaScript um statische Typen, die der Compiler vor der Ausführung prüft. Viele Typen müssen Sie nicht einmal angeben, weil TypeScript sie aus dem Code ableitet (Typinferenz):
type Status = 'offen' | 'versendet' | 'storniert';
interface Bestellung {
id: number;
kunde: string;
betrag: number;
status: Status;
}
function summe(bestellungen: Bestellung[]): number {
return bestellungen.reduce((gesamt, b) => gesamt + b.betrag, 0);
}
let anzahl = 3; // TypeScript leitet number ab
// anzahl = 'drei'; // Fehler: string ist nicht number zuweisbar
const offen: Bestellung = {
id: 1,
kunde: 'Schmitz',
betrag: 84.5,
status: 'offen',
};
console.log(summe([offen]), anzahl);
Statt eines enum für feste Wertemengen setzen viele Teams heute auf Union-Typen wie Status oben. Sie verschwinden bei der Übersetzung vollständig und erzeugen keinen zusätzlichen Code. Das passt zu einer Entwicklung, die den Alltag spürbar vereinfacht: Aktuelle Node.js-Versionen, Deno und Bun können TypeScript-Dateien direkt ausführen. Node.js entfernt dabei einfach die Typangaben, Deno und Bun übersetzen den Code im Hintergrund. Geprüft werden die Typen dabei standardmäßig nicht. Diese Aufgabe übernimmt weiterhin der TypeScript-Compiler mit tsc --noEmit, im Editor und in der CI-Pipeline. Funktionen wie enum, die echten JavaScript-Code erzeugen, funktionieren mit dem einfachen Entfernen der Typen in Node.js nicht.
Wer seine Codebasis nicht umstellen will, kann Typen auch als JSDoc-Kommentare in normale JavaScript-Dateien schreiben und sie mit der Option checkJs von TypeScript prüfen lassen. Das ist ein guter Zwischenschritt für Bestandsprojekte. Für neue Projekte mit mehreren Entwickler:innen empfehlen wir TypeScript von Anfang an: Der Mehraufwand beim Schreiben ist gering, der Gewinn bei Refactorings und Code-Reviews groß. Wie eine schrittweise Einführung gelingt, beschreibt der Beitrag TypeScript im JavaScript-Team einführen.
Was das für Ihr Team bedeutet
Ein modernes JavaScript-Projekt kommt mit erstaunlich wenigen Bausteinen aus: ES-Module, async/await, eine LTS-Version von Node.js, Vite für Webanwendungen, TypeScript für die Typprüfung, dazu Linter, Formatierer und Tests in der CI-Pipeline. Viele Pakete, die in älteren Projekten stecken, sind heute überflüssig, etwa Bibliotheken für HTTP-Anfragen, für .env-Dateien oder für einfache Hilfsfunktionen auf Arrays und Objekten. Jede entfernte Abhängigkeit ist eine weniger, die Sie aktualisieren und auf Sicherheitslücken prüfen müssen.
Überschätzt wird oft der Wechsel auf die jeweils neueste Laufzeit oder das schnellste Build-Werkzeug. Für die Qualität eines Projekts zählen eine konsistente Struktur, automatische Prüfungen und ein Team, das die Sprachkonzepte sicher beherrscht. Unterschätzt wird dagegen der Aufwand, ein gewachsenes Projekt auf ESM und TypeScript umzustellen. Hier hilft ein schrittweiser Plan, der zuerst die Werkzeugkette modernisiert und dann den Code nach und nach.
Wer die Werkzeugkette im eigenen Team auf den aktuellen Stand bringen oder Entwickler:innen gezielt schulen möchte, findet passende Angebote auf unserer Seite zu Web- und Frontend-Technologien sowie zur TypeScript-Beratung und zu TypeScript-Seminaren.
Häufige Fragen zu modernem JavaScript
Lohnt sich JavaScript noch, wenn ohnehin alle TypeScript schreiben?
Ja, denn TypeScript ist JavaScript mit Typangaben. Wer TypeScript schreibt, schreibt im Kern JavaScript, und zur Laufzeit wird immer JavaScript ausgeführt. Ohne solides Verständnis von Modulen, Referenzen und asynchronem Code helfen auch die besten Typen nur begrenzt.
Brauchen kleine Projekte überhaupt einen Build-Schritt?
Nicht unbedingt. Eine kleine Webseite mit etwas Interaktivität kommt mit ES-Modulen aus, die der Browser direkt lädt. Ein Bundler lohnt sich, sobald npm-Pakete, TypeScript, ein Framework oder viele Module im Spiel sind und Ladezeiten wichtig werden.
Welche Node.js-Version sollte ein Projekt verwenden?
Für den produktiven Einsatz eine Version mit Langzeitunterstützung, die noch aktiv gepflegt wird. Bis Node.js 25 waren ungerade Versionsnummern kurzlebige Zwischenversionen zum Ausprobieren neuer Funktionen. Ab Node.js 27 erscheint nur noch eine Hauptversion pro Jahr, und jede wird zur LTS-Version. Die geplanten Support-Enden veröffentlicht das Node.js-Projekt auf seiner Website, und Teams sollten sie im Kalender haben, um Upgrades rechtzeitig einzuplanen.
Ist Webpack in bestehenden Projekten ein Problem?
Nein. Webpack wird weiterhin gepflegt und funktioniert. Ein Umstieg auf Vite bringt vor allem schnellere Entwicklungszyklen, kostet aber Zeit für die Umstellung der Konfiguration. Bei großen, stabil laufenden Projekten lohnt er sich nur, wenn lange Build-Zeiten das Team spürbar ausbremsen.
Kategorien