Organisation von JavaScripten: Voraussetzungen und Überblick

Einleitung: Anforderungen an die heutige JavaScript-Programmierung

Dieser Teil der Einführung in JavaScript richtet sich an Web- und Anwendungsentwickler, die umfangreiche und komplexe JavaScripte schreiben. Mittlerweile ermöglichen Frameworks wie jQuery einen schnellen Einstieg in das DOM-Scripting. Wie gut der Code in diesen kleinen Anwendungsfällen strukturiert ist, spielt keine entscheidende Rolle. Das Framework gibt bereits eine Grundstruktur vor, die für diese Zwecke ausreicht.

Wenn Scripte jedoch über ein simples DOM-Scripting hinausgehen und ganze Webanwendungen entstehen, stellen sich verschiedene Fragen: Wie lässt sich die komplexe Funktionalität geordnet in JavaScript implementieren? Welche Möglichkeiten der objektorientierten Programmierung in JavaScript gibt es? Wie lassen sich Datenmodelle in JavaScript angemessen umsetzen?

Eine sinnvolle Struktur wird zur Anforderung ebenso wie die Optimierung der Performance. Wird der JavaScript-Code mehrere tausend Zeilen lang, so soll er wartbar, übersichtlich und testbar bleiben. Er soll modular sein sowie wenig Redundanzen besitzen. Verschiedene Programmierer sollen gleichzeitig daran arbeiten können. Wenn das Script veröffentlicht werden soll, sodass es Fremde auf Ihren Sites verwenden, muss es in verschiedenen, unberechenbaren Umgebungen robust arbeiten.

Diese Einführung soll Strategien vorstellen, um diesen Herausforderungen zu begegnen. Bevor wir uns konkreten JavaScript-Fragen widmen, soll zunächst erörtert werden, wann und wie JavaScript sinnvoll eingesetzt wird. Dies hat direkte Konsequenzen auf die Struktur und Arbeitsweise der JavaScripte. Deshalb ist es bereits auf dieser Ebene wichtig, die richtigen Weichen zu stellen. Alle weiteren Tipps gehen davon aus, dass Sie gemäß diesen Prinzipen arbeiten.

Das Schichtenmodell: Trennung von Inhalt, Präsentation und Verhalten

Im modernen Webdesign kommt den Webtechniken HTML, CSS und JavaScript jeweils eine bestimmte Rolle zu. HTML soll die Texte bedeutungsvoll strukturieren, indem z.B. Überschriften, Listen, Absätze, Datentabellen, Abschnitte, Hervorhebungen, Zitate usw. als solche ausgezeichnet werden. CSS definiert die Regeln für die Darstellung dieser Inhalte, sei es auf einem Desktop-Bildschirm, auf einem mobilen Gerät oder beim Ausdrucken.

Um eine Website effizient zu entwickeln sowie sie nachträglich mit geringem Aufwand pflegen zu können, sollen diese beiden Aufgaben strikt voneinander getrennt werden: Im HTML-Code werden keine Angaben zur Präsentation gemacht. Im Stylesheet befinden sich demnach alle Angaben zur Präsentation in möglichst kompakter Weise. Dadurch müssen im HTML-Code nur genau soviele Angriffspunkte für CSS-Selektoren gesetzt werden, wie gerade nötig sind (z.B. zusätzliche div- oder span-Elemente sowie id- und class-Attribute). Ein und dasselbe Dokument kann auf diese Weise durch den Wechsel des Stylesheets ein völlig anderes Layout bekommen. Aber auch ganz ohne Stylesheet sind die Inhalte noch sinnvoll strukturiert und die Inhalte zugänglich.

Erste Schritte zum Unobtrusive JavaScript

JavaScript kommt im Schichtenmodell die Aufgabe zu, dem Dokument Verhalten (engl. Behaviour) hinzuzufügen. Damit ist gemeint, dass das Dokument auf gewisse Anwenderereignisse reagiert und z.B. Änderungen im Dokument vornimmt.

Die Grundlage für eine sinnvolle JavaScript-Programmierung ist die Auslagerung des JavaScript-Codes: Im HTML-Code sollte sich kein JavaScript in Form von Event-Handler-Attributen befinden (onload, onclick, onmouseover usw.). Stattdessen werden Elemente, denen ein bestimmtes Verhalten hinzugefügt werden soll, falls nötig mit einer Klasse oder ID markiert, um die Elemente eindeutung adressieren zu können. Die nötige Interaktivität wird dem Dokument automatisch hinzugefügt. Beim Ladens des Dokuments wird das Script aktiv, initialisiert sich und startet die Ereignisüberwachung an den betreffenden Elementen. Diese Anwendung von JavaScript nennt sich Unobtrusive JavaScript, »unaufdringliches« JavaScript.

JavaScript auslagern und extern am Dokumentende einbinden

Vermeiden Sie auf ins HTML eingebetteten JavaScript-Code. Verzichten Sie auf Inline-Event-Handler und script-Elemente, die direkt JavaScript enthalten. Binden Sie externe Scripte mit <script type="text/javascript" src="..."></script> ein. Aus Performance-Gründen sollten Sie dieses script-Element ans Dokumentende setzen, direkt vor den schließenden </body>-Tag.

Scripte bei DOM Ready initialisieren

Initialisieren Sie Ihre Script zu dem Zeitpunkt, wenn das HTML-Dokument eingelesen wurde und das DOM vollständig verfügbar ist. Dieser Zeitpunkt wird üblicherweise DOM ready genannt. Siehe Onload-Techniken: Scripte ausführen, sobald das Dokument verfügbar ist. Die üblichen JavaScript-Frameworks stellen dafür Methoden bereit. Intern arbeiten diese hauptsächlich mit dem DOMContentLoaded-Event. Wenn Sie kein Framework verwenden, gibt es auch lose Helferfunktionen für diesen Zweck, z.B. ContentLoaded von Diego Perini.

Beispiel jQuery:

$(function () {
    // ...
});

Übergabe einer Funktion über ihren Namen:

$(funktion);

DOM-Zugriffe über Selektor-Engines

Nutzen Sie JavaScript-Selektor-Engines, um auf das Dokument über CSS-artige Selektoren zuzugreifen und Elemente auszuwählen. Alle verbreiteten Frameworks bringen solche mit, sie sind aber auch separat erhältlich (z.B. Sizzle).

Beispiel jQuery:

$(function () {
    // Liefert alle li-Elemente im Element mit der ID produktliste
    $('#produktliste li')
});

Zeitgemäßes Event-Handling

Nach den Ansprechen der Elemente können Sie Event-Handler registrieren. Dazu benötigen Sie eine leistungsfähige Event-Handling-Komponente, die sicherstellt, dass mehreren Handler für einen Typ registriert werden können und die Ihnen die Event-Verarbeitung durch Nivellierung von Browserunterschieden vereinfacht. Eine solche ist in den üblichen Frameworks eingebaut, alternativ können Sie eine lose addEvent-Helferfunktionen nutzen.

Beispiel jQuery:

$(function () {
    $('#produktliste li').click(function (e) {
        // ... Verarbeite Ereignis ...
    });
});

Zum effizienten Event-Handling gehört Event Delegation hinzu. Die Grundidee ist, dass Ereignisse bei einer ganzen Schar von Elementen durch ein darüberliegendes Element verarbeitet werden. Dazu macht man sich das Event Bubbling und Event Capturing zunutze. Beispielsweise jQuery bietet dafür die Methode on().

Objektorientierte Programmierung (OOP) in JavaScript

JavaScript zieht viele Programmieranfänger an und ist für viele Webautoren die erste Programmiersprache, mit der sie aktiv in Kontakt kommen. Für sie bleibt die genaue Funktionsweise von JavaScript zunächst unzugänglich. Sie treffen auf Fallstricke und schaffen es nicht, JavaScript zu bändigen. Aber auch professionelle Software-Entwickler mit fundierten Kenntnissen anderer Programmiersprachen geraten an JavaScript. Sie sind nicht weniger verwirrt und verzweifeln, weil sie in JavaScript nicht Strukturen wiederfinden, die ihnen in anderen Sprachen Orientierung bieten.

Klassenbasierte Objektorientierung in anderen Programmiersprachen

Viele verbreitete Programmiersprachen arbeiten zumindest teilweise objektorientiert und besitzen ausgereifte konventielle Konzepte zur Strukturierung von Programmen. Das sind vor allem Klassen und ferner Module, Pakete bzw. Namensräume.

Klassen werden üblicherweise mittels einer Deklaration beschrieben und können daraufhin verwendet werden. Sie bestehen aus Konstruktoren, Methoden und Eigenschaften. Sie können von anderen Klassen erben und diese erweitern. Von Klassen lassen sich beliebig viele Instanzen anlegen – oder nur eine Instanz im Falle von Singletons. Die Sichtbarkeit bzw. Verfügbarkeit von Methoden und Eigenschaften kann über Schlüsselwörter geregelt werden. So wird z.B. zwischen öffentlichen und privaten Eigenschaften und Methoden unterschieden. Eigenschaften können als nur lesbare Konstanten definiert werden. Darüber hinaus erlauben einige Programmiersprachen die Definition von Interfaces, ein Anforderungskatalog mit Methoden, die eine Klasse bereitstellen muss. Verbreitet sind ferner statische Methoden (Klassenmethoden) sowie Getter- und Setter-Methoden, die beim Lesen bzw. Schreiben von Eigenschaften aufgerufen werden.

Diese konventionelle klassenbasierte OOP sieht beispielsweise in PHP 5.3 folgendermaßen aus:

namespace Beispielnamensraum;

interface IBeispielInterface
{
    public function oeffentlicheMethode();
}

class Beispielklasse implements IBeispielInterface
{
    function __construct() {
        echo "Konstruktor\n";
    }

    public $oeffentlich = 'Öffentliche Eigenschaft';
    private $privat = 'Private Eigenschaft';

    public function oeffentlicheMethode() {
        echo "Öffentliche Methode\n";
        $this->privateMethode();
    }
    private function privateMethode() {
        echo "Private Methode\n";
        echo $this->privat . "\n";
    }

    const konstante = 'Klassenkonstante';
    public static $statisch = 'Statische Eigenschaft (Klasseneigenschaft)';
    public static function statischeMethode() {
        echo "Statische Methode (Klassenmethode)\n";
        echo self::$statisch . "\n";
        echo self::konstante . "\n";
    }
}

class AbgeleiteteKlasse extends Beispielklasse
{
    function __construct() {
        parent::__construct();
        print "Konstruktor von AbgeleiteteKlasse\n";
    }

    public function zusatzmethode() {
        echo "Zusatzmethode von AbgeleiteteKlasse\n";
        parent::oeffentlicheMethode();
    }
}

$instanz = new Beispielklasse();
$instanz->oeffentlicheMethode();
echo $instanz->oeffentlich . "\n";

echo Beispielklasse::statischeMethode();

$instanz2 = new AbgeleiteteKlasse();
$instanz2->zusatzmethode();

Dieses Beispiel soll hier nicht näher beschrieben werden, sondern nur den Hintergrund illustrieren für diejenigen, die solche oder ähnliche klassenbasierte OOP bereits kennen.

Die besagten Konzepte bilden in eine Richtschnur: Hält sich ein Programmierer an diese Konventionen, so ist mehr oder weniger garantiert, dass das Programm von anderen grob verstanden und wiederverwendet werden kann und nicht mit anderen Programmen in die Quere kommt. Selbst Sprachen, die nicht konsequent objektorientiert arbeiten, bekommen eine einheitliche Struktur dadurch, dass die Programmierer ihren Code in Namensräumen und Klassen organisieren.

JavaScript ist nicht fixiert auf Klassen

ECMAScript 3, der JavaScript zugrunde liegende Webstandard, besitzt diese Features nicht. Der Nachfolger ECMAScript 5 verbessert die eigentümlichen Fähigkeiten, bietet etwa Methoden zur Datenkapselung. Erst ECMAScript 6 führt Klassendeklarationen ein, die jedoch lediglich eine Kurzschreibweise für Konstruktoren und Prototypen ist und sich stark von der klassenbasierten Objektorientierung anderer Programmiersprachen unterscheidet.

JavaScript bringt von Haus aus keine »einzig wahre« Programmiertechnik für objektorientierte Programmierung mit. Das kann man als Nachteil auffassen, aber auch als Vorteil. Weil JavaScript ein vorgegebenes Gerüst zu fehlen scheint, fehlt Einsteigern die Orientierung. Es gibt nicht den einen vordefinierten Weg, ein Programme in JavaScript zu strukturieren. Stattdessen muss man sich selbst über die sinnvolle Organisation von JavaScripten Gedanken machen.

Glücklicherweise ist JavaScript so leistungsfähig, dass sich die Strukturen wie Klassen und Namensräume bzw. Module durchaus umsetzen lassen. Die Klassendeklarationen in ECMAScript 6 vereinfacht den Einstieg für diejenigen, die bereits andere, klassenbasierte Programmiersprachen beherrschen. Zusatzbibliotheken ermöglichen die Anwendung komplexere OOP-Konzepte in JavaScript.

Dabei sollten allerdings nicht die Eigenarten und besonderen Fähigkeiten von JavaScript vergessen werden. Im Gegensatz zu manchen streng klassenbasierten Sprachen ist JavaScript äußerst dynamisch und besitzt funktionale Aspekte. JavaScript-Kenner empfehlen, die Fähigkeiten von JavaScript zu nutzen, anstatt bloß klassenbasierte OOP überzustülpen. Mit diesen Möglichkeiten ist JavaScript nicht unbedingt schlechter und defizitär – es ist lediglich ein anderer, nicht minder interessanter und brauchbarer Ansatz.

Grundpfeiler der fortgeschrittenen JavaScript-Programmierung

Die Grundlage von ECMAScript 3 sind Objekte, die zur Laufzeit beliebig um Eigenschaften und Methoden ergänzt werden können. Ein JavaScript-Objekt ist eine ungeordnete Liste, in der String-Schlüsseln beliebige Werte zugeordnet werden. Diese dynamischen, erweiterbaren Objekte ermöglicht eine Strukturierung von Programmen. Sie dienen, wie wir später sehen werden, als vielseitiges Mittel zur Gruppierung und sind insofern mit Hashes, Namensräumen, Singletons bzw. Klassen mit statischen Methoden in anderen Sprachen vergleichbar.

Die wirklichen Stärken von JavaScript liegen jedoch woanders: Funktionen. Dazu Douglas Crockford in dem Vortrag Crockford on JavaScript – Act III: Function the Ultimate:

Functions are the very best part of JavaScript. It's where most of the power is, it's where the beauty is. … Function is the key idea in JavaScript. It's what makes it so good and so powerful. In other languages you've got lots of things: you've got methods, classes, constructors, modules, and more. In JavaScript there's just function, and function does all of those things and more. That's not a deficiency, that's actually a wonderful thing — having one thing that can do a lot, and can do it brilliantly, at scale, that's what functions do in this language.

Crockford weist hier darauf hin, wie vielseitig Funktionen in JavaScript sind und dass sie Aufgaben übernehmen, für die es in anderen Sprachen unterschiedliche Konzepte gibt.

Das folgende Diagramm soll die Säulen der fortgeschrittenen JavaScript-Programmierung illustrieren. Das Fundament bilden dynamische Objekte und Object-Literale, darauf bauen Funktionen auf, welche Scopes erzeugen und als Konstruktoren prototypische Vererbung ermöglichen.

Objekte / Object-Objekte

  • Alles ist ein Objekt – bis auf Primitives, die sich allerdings wie Objekte verhalten können
  • Objekte sind i.d.R. erweiterbar (ECMAScript 3)
  • Object-Objekte sind Allround-Container und Hashes
  • Object-Literale erzeugen mit { key : value, … }

Funktionen

  • Dienen der Codestrukturierung
  • Objekte erster Klasse: Zur Laufzeit erzeugen, übergeben, zurückgeben, in Variablen speichern
  • Funktionale Programmierung (z.B. Listenoperationen, Currying, Event-Handler)
  • Können an Objekte als Methoden gehängt werden
  • Methoden haben this-Kontext, über call und apply veränderbar (Binding)
  • Erzeugen Scopes
  • Können verschachtelt werden (Scope-Chain)
  • Dienen als Konstruktoren
  • Besitzen Prototypen für damit erzeugte Objekte (funktion.prototype)

Scope

  • Gültigkeitsbereich für Variablen
  • Allzweckwerkzeug für Datenverfügbarkeit und Kapselung/Privatheit
  • Auflösung von Bezeichnern zu Werten erfolgt über die Scope-Chain
  • Scope-Chain speichert die internen Variablenobjekte von Funktionsaufrufen
  • Scope-Chain ermöglicht Closures

Prototypen

  • Prototypenbasierte Vererbung
  • objekt.eigenschaft wird über die Prototype-Chain aufgelöst
  • Ein Objekt stellt Funktionalität für ein anderes bereit (Delegation)
  • Speichereffizientes Erzeugen von Objekten gleicher Funktionalität
  • Ableiten und Verfeinern von Funktionalität
  • Ganz normale Objekte, die im Programm und nicht per Deklaration erzeugt werden
  • Alle Objekte können Prototypen sein

Dieses Diagramm soll Ihnen einen kompakten Überblick geben. Viele der Begriffe wird diese Einführung im weiteren erläutern, auf andere kann sie nicht eingehen.