Tel. 06151 / 39 10 793

Objektorientierung in JavaScript

02.07.2012  • Thiemo Müller 1

In diesem Blogeintrag geht es um die Möglichkeiten und den Nutzen objektorientierter Programmierung in JavaScript. Ich werde im Folgenden aufzeigen, was die konkreten Vorteile gegenüber klassischer JavaScript Programmierung sind und in welchen Fällen sie anzuraten ist bzw. wann die herkömmliche JavaScript Programmierung ausreicht.

Der Artikel ist technisch orientiert, bietet aber auch Projektmanagern die Möglichkeit, sich über die aktuellen Entwicklungen auf dem Laufenden zu halten.

Vorwort

JavaScript ist eine „prototyp-basierte“ Programmiersprache und die klassische clientseitige Web-Sprache schlechthin. JavaScript gibt es bereits seit 1995 und ist seiner ganz eigenen objektähnlichen Syntax stets treu geblieben. Heute implementieren alle modernen Browser JavaScript, wenngleich in teilweise noch unterschiedlichen Versionen.

Zwar bieten alle Browser die Möglichkeit an, JavaScript auszuschalten (beispielsweise um vor nervigen Pop-Ups zu schützen), jedoch sind Sicherheitsbedenken heute nicht mehr verbreitet, da JavaScript in einer sogenannten „virtuellen Maschine“ läuft und somit keine gefährlichen Zugriffe auf den Rechner erlaubt sind.

Mittlerweile nutzen nahezu alle Webseiten JavaScript für die unterschiedlichsten Zwecke. Von einfachen „inpage-Popups“ oder dem Ein- und Ausklappen von Formularcontainern bis hin zu komplexen E-Mail Clients ist alles vertreten. Mit NodeJS gibt es auch bereits die Möglichkeit, JavaScript serverseitig einzusetzen, was neben der vereinfachten Projektverwaltung (man benutzt client- und serverseitig die gleiche Sprache) bis hin zu teilweise unglaublichen Performance-Optimierungen (Stichwort non-blocking Sockets) bereits ohne größeres Framework erhebliche Vorteile bieten kann.

Der klassische Ansatz

Wie die meisten Script-Sprachen hat auch JavaScript keinen besonders guten Ruf, was die Lesbarkeit und Wartbarkeit seines Codes angeht. Der klassische JavaScript-Programmierer ist bemüht, seiner Web-Anwendung mit ein paar Schnipseln JavaScript noch den letzten Schliff zu verleihen. Es geht in der Regel nur um kleine Aufgaben, z.B. das Aufrufen einer URL beim Klick auf einen Link. Durch Bibliotheken wie jQuery und sogenanntes „chaining“ lässt sich das mit wenigen Zeilen bewerkstelligen:

$( ‘a#fancy-link’ ).click( function( e ) {
  $( ‘#callback-container’ ).load( ‘/javascript-content’ );
  return false;
} );

„Chaining“ bezeichnet die Aneinanderreihung mehrerer Befehle am Stück ohne Trennung durch Semikolon oder Ähnliches. Indem jede Methode, die aufgerufen wird, das eigene Objekt zurückgibt, bleibt der Kontext dieses Objektes über mehrere Aufrufe hinweg erhalten:

$( ‘#my-element‘ )
  .attr( “name”, “value” )
  .click( fn )
  …;

Dies sorgt für eine erhöhte Lesbarkeit und erreichte durch jQuery stark an Popularität.

Der erfahrene Entwickler

Wer sich über die Schnipsel hinaus mit JavaScript beschäftigt, wird auch bei sorgfältigem Code-Design schnell an die natürlichen Grenzen der JavaScript-Programmierung stoßen. Doch dafür gilt es zunächst einmal, das Prinzip dieser prototypisierenden Sprache zu verstehen:

Im Gegensatz zu klassischer objektorientierter Programmierung gibt es keine strikte Trennung zwischen Klasse und Objekt. Jedes Objekt kann auch gleichzeitig als Prototyp (und damit Klasse) für ein neues Objekt dienen.

Auf der einen Seite ermöglicht das eine sehr große Freiheit, wenn man erst einmal ein Gefühl dafür entwickelt hat, wie man es sinnvoll einsetzt. Auf der anderen Seite kann es aber auch schnell verwirren, da diese strikte Trennung in klassischer objektorientierter Programmierung mehr Ordnung und damit eine bessere Lesbarkeit und ein leichteres Verständnis des Codes schafft.

Wer ein großes JavaScript-Projekt plant, sollte auf den reinen Einsatz prototypisierten Programmierens verzichten.

Mittlerweile gibt es auch einige Bibliotheken, die den Einsatz von JavaScript in größeren Projekten erleichtern oder auch erst erlauben. Darunter beispielsweise auch „Simple Inheritance“ von John Resig, dem Vater der jQuery-Bibliothek. Keine bietet allerdings den vollen Umfang moderner, objektorientierter Programmierung.

Hierzu zählen insbesondere:
- Einfache, im Optimalfall auch mehrfache, Vererbung
- Namespacing
- Virtuelle sowie statische Methoden
- Kapselung (d.h. das Verbergen der Implementierungsdetails)
- Die Verwendung des „new“ Operators bzw. allgemeine Einfachheit des Codes
- Das Nutzen bereits vorhandener Features der JavaScript Sprache

Doch auch die Vorteile, die JavaScript als Scripting-Sprache bietet, sollten nicht vernachlässigt werden. Hierzu zählt für mich das „Lazy Loading“, d.h. eine Klasse auch erst dann auflösen zu können, wenn man sie auch braucht. Das hat den Vorteil, dass man im Gegensatz zu üblicher objektorientierter Programmierung auch Basis-Klassen erst nach der Kind-Klasse einbinden kann. Das kann zwar auch schnell unübersichtlich werden, wenn man sich zu spät über die konkrete Implementierung Gedanken macht. Mir hat es allerdings auch schon einiges an Kopfzerbrechen erspart in einem Projekt, in dem entsprechend komplizierte Vererbungsketten aufgelöst werden mussten, je nach Einbindung der Anwendung.

Doch genug der Theorie.

createClass

„createClass“ ist eine von mir entwickelte JavaScript Bibliothek, die mit Ausnahme der Kapselung all die Features implementiert, die das Arbeiten mit Objektorientierung in JavaScript erleichtern. Ich habe beim Design besonderen Wert auf die einfache Anwendbarkeit sowie die gute Lesbarkeit des Codes gelegt, der mit createClass geschrieben wird. Hierzu zählt für mich auch, JavaScript als Sprache lediglich zu erweitern und nicht unnötigen Overhead oder JavaScript-untypische Konstrukte zu verwenden. Ich möchte zunächst einen groben Überblick der einfachen Features geben und anschließend auf einzelne Besonderheiten genauer eingehen.

Alle Beispiele finden Sie auch auf github. Bei Anregungen und Kritik hinterlassen Sie doch einfach einen Kommentar oder schreiben Sie mir unter mueller@brightsolutions.de. Ich freue mich, geduzt zu werden.

Beispiel 1

    createClass( "Example01.Animal" );
    Example01.Animal.prototype.sName  = null;
    Example01.Animal.prototype.Animal = function( sName ) {
      this.sName  = sName;
    };
   
    createClass( "Example01.Dog", "Example01.Animal" );
    Example01.Dog.prototype.bark      = function() {
      alert( "Dog barks:\n\nHi, my name is "+this.sName+"." );
    };
   
    var pDog  = new Example01.Dog( "Hans" );
    pDog.bark();

Im oberen Beispiel wird zunächst eine Klasse „Animal“ erstellt. Jedes Tier besitzt einen Namen, der am Objekt gespeichert wird. Wie Sie sehen, wird das klassische Prototyping von JavaScript somit weiter genutzt.

Anschließend wird die Klasse „Dog“ deklariert, die sich von „Animal“ ableitet. Beide Klassen befinden sich im Namespace „Example01“. Ein Hund kann bellen. Genau genommen seinen Namen als Dialog.

Zuletzt erzeugen wir ein Objekt des Typs „Dog“, den wir Hans nennen und bellen lassen.

Der Konstruktor für „Dog“ wird von createClass automatisch erstellt und leitet alle Argumente an den Konstruktor der Basisklasse „Animal“ weiter.

Beispiel 2

    createClass( "Example02.Animal" );
    Example02.Animal.prototype.sName        = null;
    Example02.Animal.prototype.Animal       = function( sName ) {
      this.sName    = sName;
    };
    // Demonstrate virtual functions
    Example02.Animal.prototypeVirtual( "getSpecies" );  // ():string
   
    createClass( "Example02.Dog", "Example02.Animal" );
    Example02.Dog.prototype.sOwner          = null;
    Example02.Dog.prototype.Dog             = function( sName, sOwner ) {
      this.Animal( sName );
     
      this.sOwner   = sOwner;
    };
    Example02.Dog.prototype.getSpecies      = function() {
      return "Dog";
    };
    Example02.Dog.prototype.bark            = function() {
      alert( "Dog barks:\n\nHi, my name is "+this.sName+".\nMy owner is "+this.sOwner+".\n" );
    };
   
    createClass( "Example02.BatDog", "Example02.Dog" );
    Example02.BatDog.prototype.bTransformed = false;
    Example02.BatDog.prototype.transform    = function() {
      this.bTransformed = !this.bTransformed;
    };
    Example02.BatDog.prototype.getSpecies   = function() {
      return "BatDog";
    };
    Example02.BatDog.prototype.bark         = function() {
     
      if( this.bTransformed )
        alert( "BatDog barks for justice:\n\nHi, my name is "+this.sName+".\nMy owner is "+this.sOwner+".\n" );
      else
        this.__parent.bark.apply( this );
     
    };
   
    var pDog  = new Example02.BatDog( "Hans", "Franz" );
    pDog.bark();
    pDog.transform();
    pDog.bark();
   
    alert( pDog.getSpecies() );

In diesem Beispiel wurde gezeigt, wie der Konstruktor der Basis-Klasse manuell aufgerufen wird.

Zusätzlich gibt es eine Klasse „BatDog“, die dynamisch selber bellt oder aber die Bellen-Funktion der Basis-Klasse aufruft. Wie für JavaScript üblich geschieht dies mit der Funktion .apply(). Dieser könnten ebenfalls weitere Argumente als Array übergeben werden.

Zusätzlich muss jede Tierart eine Funktion „getSpecies()“ bereitstellen, die den Namen der Gattung zurückgibt.

Beispiel 3

    var pCopyCat  = {
      prototype     : {
        somethingALotOfObjectsNeedInNoStaticWay : function() {
          console.log( 'Yes, this is dog.' );
        }
      }
    };
   
    // Third arguments enables "Lazy Loading", fourth will make it copy the pCopyCat's function
    createClass( "Example03.ChildClass", ["Example03.BaseClass","Example03.BaseClassEx"], true, pCopyCat );
    /**
     *  We only inherit this function so that iYet becomes optional.
     *  But since the base class will call .count() again, we need to use .applyEx since this is recursion.
     *
     *  @param iYet (optional) The number to count up
     */
    Example03.ChildClass.prototype.count          = function( iYet ) {
     
      console.log( "ChildClass knows how to count!" );
     
      if( typeof iYet=='undefined' )
        iYet  = 1;
     
      return this.__parent.count.applyEx( this, [ iYet ], Example03.ChildClass );
     
    };
   
    // Demonstrate recursive methods
    createClass( "Example03.BaseClass" );
    Example03.BaseClass.MAX_COUNT                 = 3;  // Static variable
    Example03.BaseClass.prototype.count           = function( iYet ) {
     
      console.log( "BaseClass knows how to count as well!" );
     
      if( iYet<Example03.BaseClass.MAX_COUNT )
        return this.count( iYet+1 );
     
      return iYet;
     
    };
   
    // Demonstrate multiple inheritance
    createClass( "Example03.BaseClassEx", "Destructable" );
    Example03.BaseClassEx.prototype.whatsYourName = function() {
      return this.__className;
    };
    Example03.BaseClassEx.prototype.$BaseClassEx  = function() {
      console.log( this.whatsYourName() + " knows how to destruct!" );
    };
   
    // Since we're using lazy loading we have to initialize the class before it's first usage
    createClass.initialize( "Example03.ChildClass" );
   
    var pChild  = new Example03.ChildClass();
    alert( pChild.whatsYourName() + " counts to " + pChild.count() );
   
    // Demonstrate difference between instanceof and .instanceOf()
    alert( pChild+":\n\n" + pChild.instanceOf(Example03.BaseClass) + " vs. " + (pChild instanceof Example03.BaseClass) + "\n" + pChild.instanceOf(Example03.BaseClassEx) + " vs. " + (pChild instanceof Example03.BaseClassEx) );
   
    // Demonstrate copy cat
    pChild.somethingALotOfObjectsNeedInNoStaticWay();
   
    // Demonstrate usage of destructors
    pChild.destroy();

Wer bis hier hin gekommen ist, bekommt gleich alle Vorteile von createClass zu spüren. Indem createClass als zweiter Parameter nicht nur der Name einer Klasse übergeben wird, sondern eine Liste aus mehreren Namen, wird multiple Vererbung genutzt. Wie Sie weiter unten sehen, wird dadurch der von JavaScript bereitgestellte „instanceof“-Operator ab der zweiten Klasse nicht mehr wirksam, da JavaScript an sich lediglich einfache Vererbung unterstützt. Verwenden Sie daher am besten immer die .instanceOf Funktion eines Objekts.

Da als dritter Parameter „true“ übergeben wird, wird „Lazy Loading“ angeschaltet. Hierdurch können wir auch Klassen, von denen wir vererben nach der vererbenden Klasse definieren („ChildClass“ erbt von „BasisClass“, die allerdings zum Zeitpunkt des Aufrufs noch gar nicht definiert wurde). Später wird die Klasse durch einen Aufruf von „createClass.instantiate()“ initialisiert und kann fortan verwendet werden. Dieses Feature sollten Sie nur mit Bedacht benutzen.

Der vierte Parameter erlaubt es, von Objekten bzw. auch Klassen sämtliche (statische wie nicht-statische) Eigenschaften und Methoden zu kopieren, ohne von der Klasse zu erben. Es können auch mehrere Objekte/Klassen als Array übergeben werden. Dieses Feature sollte ebenfalls nur vorsichtig zum Einsatz kommen, in aller Regel reicht eine einfache Vererbung bereits vollkommen aus.

Beachten Sie, dass Sie bei rekursiven Funktionen stets .applyEx() anstelle des vorher verwendeten .apply() verwenden müssen. Sollten Sie sich nicht sicher sein, verwenden Sie einfach immer .applyEx(). Der einzige Unterschied besteht in einem kleinen Performance-Vorteil von .apply(), der aber in 99% der Fälle nicht von Relevanz sein wird.

Auch Destruktoren können verwendet werden, indem von der Klasse „Destructable“ geerbt wird. Der Destruktor erhält, wie auch der Konstruktor, den Namen der Klasse, allerdings mit einem vorangestellten Dollar-Zeichen. Da JavaScript von Haus aus keinerlei Info über den Zustand eines Objektes sendet, müssen Objekte, die Destruktoren nutzen, stets mit .destroy() zerstört werden.

Parallel zum JavaScript-Äquivalent wird eine Klasse standardmäßig seinen Namen zurückgeben, wenn man es zu einem String wandelt, z.B. „[object ChildClass]“.

Fazit

Wer lediglich ein paar einfache Zeilen JavaScript für kleinere Funktionalitäten in einer Webseite benötigt, wird auch problemlos ohne Framework auskommen und ist mit jQuery bereits gut bedient.
Wer allerdings eine komplette Web-Anwendung mit starker clientseitiger Programmierung erstellen möchte, sollte sich das Leben nicht unnötig schwer machen und lieber auf eine Bibliothek setzen, mit der eine Umgebung existiert, die vergleichbare Funktionalitäten wie andere moderne Sprachen bereitstellt, wie etwa PHP, Java oder C++.

Bitte beachten Sie, dass sich createClass im Moment noch in einer offenen Beta befindet, und eventuell nicht für den Produktiveinsatz geeignet ist. Prüfen Sie dies im Zweifelsfall vor Projektbeginn oder fragen Sie nach, falls Sie weitere Hilfe, Dokumentation oder Features benötigen. Benutzen Sie hierfür die Kommentar-Funktion oder schreiben Sie mir direkt an mueller@brightsolutions.de.

Happy Coding
--Thiemo Müller

Super Blog-Artikel @Thiemo
Find ich außerdem Klasse, dass du dein cooles Framework endlich mal propagierst. :)

Kommentar hinzufügen

Der Inhalt dieses Feldes wird nicht öffentlich zugänglich angezeigt.

Suchen


Newsletter

Melden Sie sich zu unserem kostenlosen Newsletter an. Wir informieren Sie regelmässig über unsere Leistungsangebote, Drupal Best-Practices, Mobile Apps, ERPAL Updates und Veranstaltungen.

Unsere Partner
comm-press comm-press
Drupal Spezialisten aus Hamburg
neosmart - Digital Media, Webdesign & Webentwicklung<br />
aus Darmstadt neosmart
Digital Media, Webdesign & Webentwicklung aus Darmstadt
trinomica - Software Solutions trinomica
Software Solutions
Sponsoring und Mitgliedschaften

Drupal Initiative Deutschland - Firmen-Mitglied

Wir sind Diamant Sponsors für das DrupalCamp Frankfurt 2014

So finden Sie uns