JavaScript/Tutorials/OOP/Module und Kapselung

Aus SELFHTML-Wiki
< JavaScript‎ | Tutorials‎ | OOP(Weitergeleitet von Modul (ECMAScript))
Wechseln zu: Navigation, Suche
Schon in den letzten Kapiteln wurde versucht, Variablen und damit Daten so zu kapseln, dass kein Zugriff von außen möglich ist, der Störungen und Fehler verursachen könnte.

Während Skripte früher oft kleine, überschaubare Einzeiler waren, existieren heute oft mehrere komplexe Anwendungen nebeneinander.

Dieses Kapitel zeigt, wie Sie JavaScripte als Module organisieren, die in sich gekapselt sind, andererseits aber Bestandteile anderen Programmen zur Verfügung stellen können.

Das Yin und Yang von JavaScript: Kapselung vs. Verfügbarkeit

Zahlreiche Scripte im Netz sind eine lose, unstrukturierte Sammlung von dutzenden globalen Variablen und Funktionen.

Solche Scripte sind nur bedingt konfigurierbar, anpassbar und erweiterbar. Am schwersten wiegt jedoch, dass es sich um eine große Zahl von losen Objekten im globalen Scope handelt. Globale Variablen und Funktionen sind Eigenschaften des window-Objektes.[1]

sechs Eigenschaften des window-Objekts
let variable1 = "wert";
let variable2 = "wert";
let variable3 = "wert";

function funktion1 () {
    /* ... */
}
function funktion2 () {
    /* ... */
}
function funktion3 () {
    /* ... */
}

Clientseitige JavaScripte arbeiten auf einem HTML-Dokument, auf das es über das DOM zugreift. Ferner operiert es im Kontext des sogenannten globalen Objektes. Das ist in JavaScript das window-Objekt, welches den obersten Namensraum bereitstellt. Sowohl das globale Objekt als auch das DOM teilt es sich mit anderen Scripten. Diese »öffentlichen Güter« darf kein Script für sich alleine beanspruchen.

Wenn Scripte unterschiedlicher Herkunft zusammenkommen, kann das schnell zu Konflikten führen. Die Vermeidung von Konflikten setzt bereits beim Unobtrusive JavaScript an: Indem wir JavaScript-Code nicht direkt ins HTML-Dokument einbetten, sondern fortgeschrittenes Event-Handling verwenden, reduzieren wir Überschneidungen im DOM.

Konfliktfeld Nummer Eins bleibt das Window-Objekt. Darüber können zwei Scripte zusammenarbeiten, aber auch in Konflikt geraten, wenn sie gleichnamige Variablen definieren.

In JavaScript gilt es daher, ein Gleichgewicht zwischen Kapselung und Verfügbarkeit herzustellen.

  • Datenkapselung bedeutet, dass das Erweitern des globalen Objekts sowie der DOM-Objekte auf ein Minimum reduziert wird. Ein Script sollte den globalen Scope nicht für seine Arbeitsdaten verwenden und globale Variablen möglichst vermeiden. Es sollte nur die Objekte am window-Objekt speichern, die für den Zugriff von außen unbedingt vonnöten sind.
  • Eine globale Verfügbarkeit von Daten benötigt man für
    • öffentliche Programmierschnittstellen (API) besonders bei Bibliotheken
    • Konfigurierbarkeit des Scripts z. B. durch verschiedene Nutzer
    • Erweiterbarkeit (Modularisierung)

type="module" (ES6)

Module sind ein Versuch, diese Probleme zu vermeiden. Modulschnittstellen haben viel mit den Objektschnittstellen aus dem ersten Kapitel gemeinsam. Sie machen einen Teil des Moduls für die Außenwelt verfügbar und halten den Rest privat.

Die Idee von Modulen wie dem "Revealing Module Pattern", in dem man Module noch per Hand baut, ist in ECMAScript 6 (auch als ECMAScript 2015 bezeichnet) übernommen worden.[2][3][4]

ES6 Module bieten ein Konzept für Programmiermuster:

  • Ein Modul exportiert bestimmte Objekte. Das können einfach nur Objekte sein, aber auch Konstruktoren für Funktionen.
  • Ein Modul importiert optional andere Module und verwendet deren Exporte.
  • Was ein Modul nicht exportiert, ist im Modul privat.
  • Genau wie bei klassischen Tools wie require.js gibt es Tools zum Bundling und Minifizieren von ES6 Modulen, damit eine Application, die aus 20 Modulen besteht, effizient geladen werden kann.
<script type='module' src='./js/app.js'></script>

Damit eine .js Datei (oder besser: .mjs) als Modul geladen wird, muss im script-Element das Attribut type="module" gesetzt werden. Ein Script, dessen Quellcode im HTML eingebettet ist, kann ebenfalls type="module" verwenden.

Wichtig ist auch, dass das Modul mit einem Javascript MIME-Typ geliefert wird, und dass man Module von fremden Sites nur importieren kann, wenn man seine Seite mit passenden CORS Headern ausliefert.

ECMAScript-Module werden grundsätzlich im strengen Modus ausgeführt. Module, durch ein <script>-Element im HTML geladen wurden (oder von einem solchen Modul importiert wurden), werden als defer-Script nach Fertigstellung des DOM, aber vor dem Auslösen des DOMContentLoaded Events ausgeführt.

Beachten Sie: Für den Einsatz von .mjs im Web muss Ihr Webserver so konfiguriert sein, dass er Dateien mit dieser Erweiterung mit dem entsprechenden Content-Type: text/javascript-Header ausliefert (siehe oben). Außerdem sollten Sie Ihren Editor so konfigurieren, dass er .mjs-Dateien als .js-Dateien behandelt, um eine Syntaxhervorhebung zu erhalten. Die meisten modernen Editoren tun dies bereits standardmäßig.[5]

export

Jede Script-Datei ist ein eigenes Modul. Um Objekte, Funktionen, Klassen oder Variablen für die Außenwelt verfügbar zu machen, ist es einfach, sie zu exportieren und sie dann bei Bedarf in andere Dateien zu importieren.

Sie können Objekte, Funktionen, Klassen oder Variablen exportieren, indem Sie das Schlüsselwort export vor der Variablendeklaration verwenden. Auf die gleiche Weise können Sie auch eine Funktion und eine Klasse exportieren.

// export features declared elsewhere
export { myFunction2, myVariable2 };

// export individual features (can export var, let, const, function, class)
export let myVariable = Math.sqrt(2);
export function myFunction() {
  // …
}

Exportieren mit einem Alias

Mit dem Schlüsselwort as können Sie exportierten Mitgliedern auch einen Alias zuweisen:

export { myNumbers, myLogger as Logger, Alligator }

import

Sie können eine Variable mit dem Schlüsselwort import importieren. Sie können eine oder alle Variablen angeben, die Sie aus einer JavaScript-Datei importieren möchten.

import member_to_import from app.js;

// You can also use an alias while importing a member.
import Greeting as Greet from "./app.js";

// If you want to import all the members but don’t
// want to Specify them all then you can do that using
// a ' * ' star symbol.
import * as exp from "./app.js";

import() - bei Bedarf nachladen

Einige Funktionen müssen beim Start von Anwendungen nicht unbedingt verfügbar sein. Um die Ladezeit zu verkürzen, können Sie solche Funktionen in Modulen unterbringen und sie seit ES2020 mit dem funktionsähnlichen import() dynamisch laden.[6]

import(moduleSpecifier);

Mit import() können Sie ein Modul erst bei Bedarf importieren.

  • import() akzeptiert einen Modul-Spezifizierer (moduleSpecifier), der das gleiche Format hat wie der Modul-Spezifizierer, der für die import-Anweisung verwendet wird. Darüber hinaus kann der moduleSpecifier ein Ausdruck sein, der als String ausgewertet wird.
  • Die Funktion import() gibt ein Promise zurück, das erfüllt wird, sobald das Modul vollständig geladen ist.
let btn = document.querySelector('#show');

btn.addEventListener('click', function() {
    import('./dialog.js')
        .then(( dialog ) => {
            dialog.show();
        })
        .catch( error => {
            // handle error here
        });
});

Da import() ein Promise zurückgibt, können Sie async/await im app.js Modul so verwenden:

let btn = document.querySelector('#show');

btn.addEventListener('click', function () {
    (async () => {
        try {
            let dialog = await import('./dialog.js');
            dialog.show('Hi')
        } catch (error) {
            console.log(error);
        }
    })();

});

Module mit dem Objekt-Literal (ES3)

Die oben besprochenen ES6-Methoden und Schlüsselwörter erleichtern die Arbeit mit Modulen. Matthias Schäfer stellte in seiner „Einführung in JavaScript“ 2006-2008 vor, wie man dies in vielen Schritten in ES3 umsetzen kann.[1]


Eine einfache Möglichkeit, um den globalen Scope zu schonen, ist die Gruppierung aller Variablen und Funktionen eines Scripts in einer JavaScript-Objektstruktur. Im globalen Geltungsbereich taucht dann nur noch diese eine Objektstruktur auf, andere globale Variablen oder Funktionen werden nicht belegt. Das Script ist in der Objektstruktur in sich abgeschlossen. Damit sind Wechselwirkungen mit anderen Scripten ausgeschlossen, solange der Bezeichner der Objektstruktur eindeutig ist.

In JavaScript gibt es den allgemeinen Objekttyp Object, von dessen Prototypen alle anderen JavaScript-Objekte abstammen. Das heißt, jedes JavaScript-Objekt ist immer auch ein Object-Objekt. Object ist die Grundlage, auf der die restlichen spezifischeren Objekttypen aufbauen.

Ein Beispiel-Object lässt sich in der Literal-Schreibweise so umsetzen:

Objekt-Literal als Modul
let Modul = {
    eigenschaft : "wert",
    methode : function () {
        alert("Modul-Eigenschaft (über window.Modul): " + Modul.eigenschaft);
        // Alternativ:
        alert("Modul-Eigenschaft (über this): " + this.eigenschaft);
    }
};
Modul.methode();

Eine Illustration der entstehenden Verschachtelung:

  • window (globales Objekt)
    • Modul (Object)
      • eigenschaft (String)
      • methode (Function)


Der Zugriff auf die Unterobjekte des Object-Containers ist von außen über den globale Namen nach dem Schema Modul.eigenschaft möglich. Im Beispiel wird über Modul.methode() die zuvor angehängte Funktion aufgerufen.

Kapselung mit privatem Funktions-Scope

Beim Objekt-Literal wird ein globales Objekt als Namensraum benutzt, um darin eigene Objekte unterzubringen. All diese Objekte sind über das Containerobjekt für andere Scripte zugänglich. Es gibt also keine Trennung zwischen öffentlichen und privaten Daten. Während es sinnvoll ist, dass z. B. eine Methode Modul.methode() von außen aufrufbar ist, ist es unnötig und potenziell problematisch, dass jede Objekteigenschaft gelesen und manipuliert werden kann.

Der nächste Schritt ist daher, eine wirksame Kapselung zu implementieren. Das Mittel dazu ist ein eigener, privater Scope (Variablen-Gültigkeitsbereich). Darin können beliebig viele lokale Variablen und Methoden definiert werden. Die einzige Möglichkeit, in JavaScript einen Scope zu erzeugen, ist eine Funktion. Wir definieren also eine Funktion, um darin das gesamte Script zu kapseln. Solange durchgehend lokale Variablen und Funktionen verwendet werden, wird der globale Scope nicht angetastet.

Ein mittlerweile stark verbreitetes Muster ist daher folgender Codeschnipsel:

Beispiel
(function () {
    /* ... */
})();

Dies erscheint zunächst sehr kryptisch, daher eine schrittweise Zerlegung der Syntax:

  1. Erzeuge eine namenlose Funktion per Funktionsausdruck: function () { ... }
  2. Umschließe diesen Funktionsausdruck mit runden Klammern: (function () {})
  3. Führe die Funktion sofort aus mit dem Call-Operator, das sind die beiden runden Klammern: (function () { ... })(). Die Parameterliste bleibt in diesem Beispiel leer.
  4. Schließe die Anweisung mit einem ; ab.


Diese anonyme Funktion wird nur notiert, um einen Scope zu erzeugen, und sie wird sofort ausgeführt, ohne dass sie irgendwo gespeichert wird. Innerhalb der Funktion wird nun der gewünschte Code untergebracht:

Beispiel
(function () {
    
    /* Lokale Variable */
    let variable = 123;
    
    /* Lokale Funktion */
    function funktion () {
        /* ... */
    }
    
    /* Rufe lokale Funktion auf: */
    funktion();
    
    /* Zugriff auf globale Objekte ist ebenfalls möglich: */
    alert(document.title);
    
})();

Im Beispiel finden sich eine Variablendeklarationen und eine Funktionsdeklaration. Beide sind lokal, sind also nur innerhalb der Kapselfunktion zugänglich. Wir können auf die Variablen und Funktionen direkt zugreifen.

Empfehlung: Vergessen Sie nicht, Variablen mit var als lokal zu deklarieren. Andernfalls werden sie automatisch global, also Eigenschaften von window.

Das Beispiel macht noch nichts sinnvolles. Die Nützlichkeit von Funktionen zur Kapselung ergibt sich z. B. bei einem Anwendungsbeispiel mit Event-Handling.

Beispiel
(function () {
    
    let clickNumber = 0;
    let outputEl;
    
    function buttonClicked () {
        clickNumber++;
        outputEl.html('Button wurde ' + clickNumber + ' Mal angeklickt');
    }
    
    function init () {
        outputEl = jQuery('#output');
        jQuery('#button').click(buttonClicked);
    }
    
    jQuery(document).ready(init);
    
})();


Das zugehörende HTML:

Beispiel
<button id="button">Klick mich</button>
<p id="output">Button wurde noch nicht angeklickt</p>
<script src="beispiel.js"></script>

Der Code nutzt die jQuery-Bibliothek, um eine Initialisierungsfunktion bei DOM ready auszuführen. Diese registriert bei einem Button einen Event-Handler. Wird der Button geklickt, wird eine Zahl erhöht. Zudem wird die bisherige Anzahl der Klicks im Dokument ausgegeben.

Das Besondere an diesem Script sind die vier lokalen Variablen bzw. Funktionen. Sie werden direkt im Funktions-Scope notiert, anstatt sie an einen Object-Container zu hängen. Innerhalb der verschachtelten Funktionen sind die Variablen des äußeren Funktions-Scope verfügbar (siehe Closures). init() füllt die Variable outputEl und greift auf die Funktion buttonClicked() zu. buttonClicked() greift auf die Variablen clickNumber und outputEl zu. Das Script funktioniert, ohne dass Objekte am globalen window-Objekt angelegt werden.


Revealing Module Pattern

Wir haben nun beide Extreme kennengelernt: Bei Object-Containern sind alle Unterobjekte öffentlich. Bei einer Kapselfunktion ist kein Objekt nach außen hin zugänglich. Wenn wir ein wiederverwendbares Script schreiben wollen, wollen wir meist eine öffentliche Schnittstelle (API) anbieten. Dazu müssen einige ausgewählte Objekte, in der Regel Methoden, sowohl nach außen sichtbar sein als auch Zugriff auf die internen, privaten Objekte haben. Man spricht in diesem Fall von privilegierten Methoden.

Diesen Kompromiss erreichen wir durch eine Kombination aus Object-Literalen und einer Kapselfunktion. Dieses Entwurfsmuster nennt sich Revealing Module Pattern. Kurz gesagt gibt die Kapselfunktion ein Objekt nach draußen, bevor sie sich beendet. Über dieses Objekt können gewisse privilegierte Methoden aufgerufen werden.

Wir beginnen mit dem bereits beschriebenen Funktionsausdruck, der sofort ausgeführt wird:

Beispiel
(function () {
    /* ... private Objekte ... */
})();
Das Neue ist, dass diese Funktion einen Wert zurückgibt, der in einer Variable gespeichert wird:
let Modul = (function () {
    /* ... private Objekte ... */
})();


Dieser Wert ist ein Objekt, welches wir in der Funktion mit einem Objekt-Literal notieren und mittels return nach draußen geben. An dem Objekt hängen die öffentlichen Eigenschaften und Methoden:

Beispiel
let Modul = (function () {
    
    /* ... private Objekte ... */
    
    /* Gebe öffentliche API zurück: */
    return {
        öffentlicheMethode : function () { ... }
    };
    
})();


Innerhalb der anonymen Funktion notieren wir wie üblich unsere privaten Objekte. Das folgende Beispiel definiert eine öffentliche, privilegierte Methode. Sie hat Zugriff auf sämtliche internen, privaten Objekte, welche direkt von außen nicht zugänglich sind.

Beispiel
let Modul = (function () {

    // Private Objekte
    let privateVariable = "privat";
    function privateFunktion () {
        alert("privateFunktion wurde aufgerufen\n" +
            "Private Variable: " + privateVariable);
    }
    
    // Gebe öffentliches Schnittstellen-Objekt zurück
    return {
        öffentlicheMethode : function () {
            alert("öffentlicheMethode wurde aufgerufen\n" +
                "Private Variable: " + privateVariable);
            privateFunktion();
        }
    };
    
})();

// Rufe öffentliche Methode auf
Modul.öffentlicheMethode();

// Ergibt undefined, weil von außen nicht sichtbar:
window.alert("Modul.privateFunktion von außerhalb: " + Modul.privateFunktion);

Da die privilegierten Methoden innerhalb des Funktions-Scope notiert werden, haben sie darauf Zugriff. Das liegt daran, dass sie Closures sind.

Es ist natürlich möglich, solche Module nicht direkt als globale Variablen zu speichern, sondern verschiedene in einem Object-Literal zu speichern. Dieser dient dann als Namensraum für zusammengehörige Module. So ist letztlich mehrere Module unter nur einer globalen Variable gespeichert.

Beispiel
let Namensraum = {};
Namensraum.Modul1 = (function () { ... })();
Namensraum.Modul2 = (function () { ... })();

Erweiterbare Module

Ben Cherry schlägt eine Erweiterbarkeit von Modulen auf Basis des Revealing Module Patterns vor[7] . Er unterscheidet zwischen fester und lockerer Kopplung der Teile. Das heißt, entweder setzt ein Aufbaumodul ein Basismodul zwingend voraus. Oder beide Module ergänzen sich gegenseitig, sind aber auch separat funktionsfähig.


Feste Kopplung

Beispiel
/* Grundmodul */
let Modul = (function (Modul) {
    /* ... private Objekte ... */
    return {
        methode1 : function () { ... }
    };
})();

/* Erweiterung des Grundmoduls */
(function (modul) {
    /* ... private Objekte ... */
    /* Erweitere Modul um neue Methoden: */
    modul.methode2 = function () { ... };
})(Modul);

Die Definition des Grundmoduls erfolgt wie beim Revealing Module Pattern besprochen. Zur Erweiterung des Moduls wird eine weitere anonyme Funktion angelegt und ausgeführt. Diese Funktion bekommt das Modulobjekt als Parameter übergeben und fügt diesem neue Methoden hinzu oder überschreibt vorhandene. Innerhalb der Funktion können wie üblich private Objekte und Methoden angelegt werden.

Nach der Ausführung des obigen Codes besitzt das Modul zwei öffentliche Methoden:

Beispiel
Modul.methode1();
Modul.methode2();

Zu beachten ist, dass die Methoden der Erweiterung keinen Zugriff auf die privaten Objekte des Grundmoduls haben – denn sie befinden sich in einem anderen Funktions-Scope. Zur Lösung dieses Problems schlägt Ben Cherry eine Methode vor, die die privaten Objekte kurzzeitig öffentlich macht, sodass ein übergreifender Zugriff möglich ist. Das erscheint mir jedoch besonders umständlich – in diesem Fall würde ich privaten Objekte zu dauerhaft öffentlichen Eigenschaften machen und auf die vollständige Kapselung verzichten.


Lose Kopplung

Bei der losen Kopplung können die Teilmodule alleine oder zusammen stehen. Ferner ist die Reihenfolge, in der die Teilmodule notiert werden, unwichtig. Dafür können sie nicht stillschweigend auf die gegenseitigen öffentlichen Methoden zugreifen, sondern müssen gegebenenfalls prüfen, ob diese definiert sind.

Beispiel
let Modul = (function (modul) {
    /* ... private Objekte ... */
    
    /* Lege Methode am Modulobjekt an: */
    modul.methode1 = function () { ... };
    
    return modul; 
}(Modul || {}));

let Modul = (function (modul) {
    /* ... private Objekte ... */
    
    /* Lege Methode am Modulobjekt an: */
    modul.methode2 = function () { ... };
    
    return modul; 
}(Modul || {}));


Die Moduldeklarationen sind gleich aufgebaut: Es gibt eine anonyme Funktion, um einen privaten Scope zu erzeugen. Diese Funktion bekommt das bestehende Modul übergeben. Der Ausdruck Modul || {} prüft, ob das Modul bereits definiert wurde. Falls ja, wird dieses der Funktion übergeben. Andernfalls wird mit dem Object-Literal ein leeres Objekt erzeugt und übergeben. Somit ist gesichert, dass die Funktion ein Objekt als Parameter entgegennimmt. Innerhalb der Funktion können wir private Objekte notieren und das Modulobjekt um neue Eigenschaften erweitern. Am Ende wird das Modul zurückgegeben und der Rückgabewert in einer Variable gespeichert.

Das Resultat ist ebenfalls, dass das Modul zwei öffentliche Methoden besitzt:

Beispiel
Modul.methode1();
Modul.methode2();



ToDo (weitere ToDos)

  1. SELFHTML bemüht sich, Tutorials immer mit einem kontextualisierten Beispiel zu erklären.
    Wer entwickelt ein praktisches Beispiel eines Moduls, in dem Variablen ex- und importiert werden?
  2. Aus dem ES3- Abschnitt habe ich eine Dopplung zu Objekte und ihre Eigenschaften sowie die Abschnitte über jQuery entfernt. Sollte dieser Bereich weiter gekürzt werden?
  3. Abschnitt zu Node.js
--Matthias Scharwies (Diskussion) 06:53, 23. Apr. 2023 (CEST)

Quellen

  1. 1,0 1,1 Bei diesem Text handelt es sich um eine überarbeitete Übernahme aus der Einführung in JavaScript von Mathias Schäfer.
    Mathias Schäfer: Organisation von JavaScripten: Module und Kapselung
  2. SELF-Forum: Javascript: "script type='module '" Was ist das? vom 07.03.2020
  3. MDN JavaScript Modules
  4. Modules eloquentjavaScript
  5. A note on file extensions (v8.dev)
  6. Introduction to the JavaScript import() (javascripttutorial.net)
  7. Ben Cherry: JavaScript Module Pattern: In-Depth

Siehe auch