Einführung in die Grundlagen der Programmiersprache

JavaScript für .NET-Entwickler
Kommentare

JavaScript und HTML5 gewinnen zunehmend an Bedeutung bei der Entwicklung von Oberflächen moderner Anwendungen, die längst schon nicht mehr ausschließlich auf Tablets oder Smartphones laufen. Im ersten Teil dieser Reihe stand ein Überblick über die bekanntesten Bibliotheken und Frameworks sowie die Architektur einer JavaScript-Anwendung im Vordergrund. Diesmal soll die Programmiersprache JavaScript selbst etwas genauer betrachtet werden und zwar aus dem Blickwinkel eines .NET-Entwicklers.

Die Programmiersprache JavaScript wurde ursprünglich von der Firma Netscape unter dem Namen LiveScript entwickelt. Sie war vor allem darauf ausgelegt, einfache Interaktivitäten durch Manipulation des DOMs auf der Webseite hineinzubringen. Seitdem hat sich an den Grundlagen der Sprache selbst relativ wenig verändert, abgesehen von kleineren Erweiterungen mit neuen Versionen. Was sich jedoch seit der Einführung der Sprache grundlegend gewandelt hat, sind die verschiedenen Einsatzgebiete von JavaScript.

Neben Bibliotheken und Frameworks, die den Entwickler auf der Clientseite bei der Entwicklung moderner Oberflächen unterstützen, haben sich auch Technologien und damit Bibliotheken herausgebildet, die einen serverseitigen Einsatz von JavaScript ermöglichen. Dazu zählt etwa Node.js. Projekte wie node-webkit ermöglichen darüber hinaus sogar die Entwicklung plattformübergreifender Desktopanwendungen in JavaScript. Eines haben alle diese Anwendungen jedoch gemeinsam: die Programmiersprache JavaScript.

Auf den ersten Blick betrachtet scheint JavaScript anderen „Klammersprachen“ wie C#, C++ oder auch Java sehr ähnlich. Bei genauerer Betrachtung ergeben sich jedoch größere Unterschiede, die besonders aufgrund der syntaktischen Ähnlichkeit immer wieder zu Verwirrung führen.

JavaScript ist eine dynamisch typisierte, objektorientierte, aber klassenlose Programmiersprache. Trotzdem lassen sich Objekthierarchien und Vererbung mithilfe der so genannten prototypischen Vererbung realisieren. Dazu kommen weitere Besonderheiten wie der Funktions-Scope, die automatische Konvertierung von Typen sowie das teilweise automatische Einfügen von Semikolons.

Das Typsystem: Dynamische Typisierung

Einer der größten Kritikpunkte an JavaScript ist immer wieder, dass diese Sprache keine strenge Typisierung verwendet, wie wir sie von den .NET-Sprachen, C/C++ oder auch Java kennen. In JavaScript nimmt eine Variable nach jeder Zuweisung eines Werts jeweils den Typ des zugewiesenen Werts an. Dazu kommt, dass Objekte zu jedem Zeitpunkt erweitert und verändert werden können. Damit ist zum Zeitpunkt des Erstellens keine statische Typprüfung des Programmcodes, wie sie beispielsweise ein C#-Compiler durchführt, möglich, da erst zum Zeitpunkt des Zugriffs bekannt ist, über welche Eigenschaften der Typ dann verfügt. Abhilfe schaffen hier Sprachen, die nach JavaScript kompiliert werden können, wie beispielsweise CoffeeScript, oder Erweiterungen der Programmiersprache JavaScript, wie sie TypeScript ermöglicht. Diese Programmiersprachen sollen später in einem separaten Artikel betrachtet werden. Hier geht es erst einmal um die Sprache JavaScript an sich.

Welche Typen gibt es eigentlich in JavaScript? JavaScript kennt insgesamt nur vier bzw. fünf Typen (Number, String, Boolean, Object und Undefined), wenn man „Undefiniert“ mitzählt. Dabei wird nicht explizit in ganzzahlige und Gleitkommawerte unterscheiden. Alle Zahlen sind ausschließlich vom Typ Number, der intern als 64-Bit-Gleitkomma-Wert nach dem IEEE-754-(double)-Standard gilt. Um auszudrücken, dass ein Wert keine gültige Zahl ist, wird die Konstante Number.NaN (Not a Number) verwendet. Hierbei ist zu beachten, dass diese Konstante nicht auf Gleichheit geprüft werden kann. NaA ist nicht gleich NaN. Um zu überprüfen, ob eine Variable einen ungültigen Wert enthält, muss die Funktion Number.isNaN(variable) verwendet werden. Andere Typen lassen sich mithilfe der Funktion Number(variable) in eine Zahl umwandeln.

Zeichenketten sind in JavaScript 16-Bit-Zeichen nach der UCS-2-Kodierung (nicht UTF-16). Es gibt keinen separaten Typ, um ein einzelnes Zeichen dazustellen, ein Zeichen ist eine Zeichenkette mit der Länge 1. Alle Zeichenketten sind (wie in vielen Programmiersprachen) unveränderlich. Die Gleichheit von zwei Zeichenketten kann mithilfe des Gleichheitsoperators (==) geprüft werden. Andere Typen lassen sich mithilfe der Funktion String(variable) in eine Zeichenkette umwandeln.

Wahrheitswerte werden mithilfe des Typs Boolean abgebildet. In JavaScript ist dieser Typ nicht von einem anderen Typen (z. B. Ganzzahl (int)) abgeleitet. Es gibt nur zwei Werte, die dieser Typ annehmen kann, nämlich true und false. Alle Typen können auch unter Verwendung der Funktion Boolean(variable) in einen Wahrheitswert umgewandelt werden. Bei der Umwandlung nach Boolean werden folgende Werte immer nach false gewandelt:

false, null, undefined, "" (leere Zeichenkette), 0, NaN

Alle anderen Werte einschließlich „0“ oder „false“ werden nach true evaluiert.

Da die Typzuweisung in JavaScript dynamisch erfolgt, muss es einen Typ für Variablen geben, den diese haben, bevor Ihnen der erste Wert zugewiesen wurde. Dieser Typ heißt undefined. Alle verbleibendenden Elemente sind vom Type Object, einschließlich Arrays, Regular Expressions und Funktionen.

JavaScript verfügt über eine automatische Typkonvertierung, die immer dann zum Einsatz kommt, wenn ein Ausdruck mit den aktuellen Typen nicht ausgewertet werden kann. Deshalb ist es wichtig zu wissen, wie diese Programmiersprache sich verhält. Da viele der Operatoren mehrfache Bedeutungen haben, die jeweils von den Typen auf der linken und rechten Seite abhängen, ist es wichtig, die Typen ggf. explizit umzuwandeln, bevor Operatoren zur Anwendung kommen.

So ergibt der Vergleich der Werte „42“ und 42 mithilfe des einfachen Gleichheitsoperators (==) true. Da sich eine Zeichenkette und eine Zahl nicht direkt vergleichen lassen, wird die Zahl automatisch in eine Zeichenkette umgewandelt. Ja nach Anwendungsfall kann dieses Verhalten gewünscht oder auch nicht gewünscht sein. Um sicherzustellen, dass neben dem Inhalt auch der Typ mit überprüft wird, stellt JavaScript einen zweiten strengen Gleichheitsoperator (===) zur Verfügung. Dieser prüft neben dem Inhalt auch den Typ. „42“ === 42 ergibt false.

Funktionen und Objekte

Funktionen sind in JavaScript Werte erster Ordnung. Das bedeutet, Funktionen können wie andere Werte auch behandelt werden, indem diese z. B. einer Variablen zugewiesen oder als Parameter in eine andere Funktion übergeben werden (Listing 1).

var add = funktion(a,b) { return a + b }
var wrapper = function(func){ func() ; }
wrapper(function() {
  var res = func(3,4);
  console.write(res);
}

Objekte sind in JavaScript eine Sammlung von Schlüssel-Wert-Paaren (KeyValue Pairs). Ein neues Objekt kann unter anderem direkt mithilfe des Objekt-Literals { } angelegt werden. Da diese Programmiersprache dynamisch ist, können Eigenschaften entweder gleich oder später hinzugefügt werden. Die beiden Objekte obj und obj2 im Beispiel aus Listing 2 sind identisch.

var obj = { };
obj.a = 42;
obj.b = "Hallo JavaScript";
obj.c = function() { return 5 ;}

var obj2 = { 
  a : 42,
  b : "Hallo JavaScript",
  c : function () { return 5 ;}
}

Eine weitere Besonderheit stellt der this-Operator in JavaScript dar. Im Gegensatz zu anderen Programmiersprachen ist der this-Operator immer an das Objekt gebunden, das die Funktion auch aufgerufen hat. Das hat Auswirkungen auf die Zuweisung von Funktionen von einem Objekt zum anderen. In Listing 3 soll eine Funktion erstellt werden, die eine Variable mit einem konstanten Wert addiert. Der konstante Wert ist als offset in dem Objekt angegeben.

var calcDemo {
  offset : 10,
  calculate : function(x) {
    return this.offset + x;
  }
}
calcDemo.calculate(10) // ergibt 20;
// Zuweisen der Berechnungsfunktion in eine Variable
var calcFunction = calcDemo.calculate;
calcFunction(10) // ergibt NaN, weil this nicht an calcDemo gebunden ist.

Wird die Berechnungsfunktion mit dem Objekt calcDemo aufgerufen, an dem auch offset definiert ist, kann offset ausgewertet werden, da this an dieses Objekt gebunden ist. Weist man anschließend einer anderen Variablen calcFunction die Berechnungsfunktion zu und ruft diese direkt auf, ist this nicht länger an calcDemo, sondern an das aufrufende Objekt der Funktion gebunden. Wird kein aufrufendes Objekt angegeben, ist das aufrufende Objekt automatisch das globale Objekt (Window). Da in Window aber kein Wert offset existiert, oder schlimmer noch, falls ein Wert offset existiert, der aber nicht gemeint ist, kann der Wert nicht korrekt ausgewertet werden. Damit ist das Ergebnis bei der zweiten Addition aus Listing 3 NaN. Dieses Verhalten wird auch als „Global-Leakage“ bezeichnet.

[ header = Seite 2: Von Prototypen und Vererbung ]

Von Prototypen und Vererbung

In JavaScript verfügt jedes Objekt über einen Prototyp. Dies lässt sich am besten mithilfe von zwei/drei einfachen Objekten für einen 2d- bzw. 3d-Point und eine Lokation zeigen. Im folgenden Beispiel wird zunächst das Objekt point2d mit den Eigenschaften x und y erstellt. Anschließend wird ein weiteres Objekt point3d erstellt und diesem Objekt die Eigenschaft z hinzugefügt:

var point2d = { x:10, y:20 };
var point3d = Object.create(point2d);
point3d.z = 30;

Zunächst erbt das Objekt point3d die Eigenschaften (x und y) von dem Objekt point2d, weil dieses als Prototyp für das Objekt point3d angegeben wurde. Wird kein Prototyp angegeben, erbt das neue Objekt immer von Object.prototype. Auf diese Weise werden die Basisfunktionen und Eigenschaften für Objekte an das Objekt point2d und somit auch an das Objekt point3d vererbt.

Abb. 1: Objekt „point3d“ erbt von „point2d“ und „Object.prototype“

Als Nächstes wird ein weiteres Objekt location hinzugefügt. Dieses Objekt erbt auch alle Eigenschaften von point2d:

var location = Object.create(point2d);
location.name = "Berlin"

Abb. 2: Objekt „point3d“ erbt von „point2d“ und „Object.prototype“

Das Besondere bei dieser Art der Vergebung ist: Wenn eine Eigenschaft zu dem Objekt point2d hinzugefügt oder der Wert einer der zwei Eigenschaften x und y verändert wird, so vererbt sich diese Änderung auf alle Objekte, die point2d als Prototyp haben.

Wird hingegen die Eigenschaft x in point3d oder location geändert, wird ein neues x im jeweiligen Objekt angelegt und damit die entsprechende Eigenschaft nicht länger vererbt, sondern einfach überschrieben. Diese spezielle Art der „Vorlagen“-basierten Vererbung wird prototypische Vererbung genannt.

Objekte können durch Kombination des new-Operators mit einer Funktion auch direkt nach dem Erstellen initiiert werden. Diese Funktion fungiert dabei quasi wie ein C-tor in der klassenbasierten Vererbung.

Wird der new-Operator zusammen mit einer Funktion verwendet, erzeugt JavaScript zunächst ein leeres Objekt { }, das anschließend an die angegebene Funktion als this-Operator übergeben wird. Ferner wird der Prototyp immer automatisch von dem Namen der Funktion nach dem Schema Funktionsname.prototype abgeleitet. In JavaScript gibt es eine Konvention, solche Funktionen großzuschreiben, die Objekte initialisieren.

Im folgenden Beispiel soll eine Funktion entstehen, die eine Person mit einem Vor- und einem Nachnamen erstellt:

function Person(firstName,lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

Anschließend werden zwei Personen mithilfe des new-Operators und der Funktion Person angelegt. Weiterhin wird eine Funktion getFullName an den Prototyp aller Personen erstellt:

var max = new Person("Max", "Mustermann");
var susi = new Person("Susi", "Sorglos");

Person.prototype.getFullName = function() {
  return this.firstName + " " + this.lastName;
}

Da JavaScript eine dynamische prototypenbasierte Sprache ist, spielt es keine Rolle, ob die gemeinsame Funktion vor oder nach dem Anlegen der einzelnen Personen erstellt wird, denn diese wird sofort nach dem Erstellen an alle Personen vererbt. Auf diese Weise lassen sich bereits vorhandene Funktionen nachträglich um zusätzliche Objekte erweitern.

Variablen-Scope und Revealing Modul Pattern

Im Gegensatz zu vielen anderen Programmiersprachen hat JavaScript keinen Block-Scope für Variablen, sondern einen Function Scope. Das bedeutet, dass sich die Sichtbarkeit einer Variablen nicht auf den aktuellen Block, sondern auf die aktuelle Funktion beschränkt. Welche Auswirkungen das haben kann, soll in einem kleinen Beispiel etwas genauer betrachtet werden. JavaScript unterscheidet in den globalen Scope und die jeweiligen lokalen Scopes. Eine globale Variable wird angelegt, indem diese außerhalb einer Funktion oder ohne Angabe des Schlüsselworts var erzeugt wird.

In Listing 4 werden die globalen Variablen a und b auf verschiedene Weise außerhalb der Funktion someFunction angelegt. Anschließend wird a auf die Konsole ausgegeben. Der Programmzweig, in dem die lokale Variable a erzeugt wird, kommt nie zur Ausführung. Im alternativen Programmzweig wird eine lokale Variable b angelegt, die anschließend ausgegeben wird. Welche drei Ausgaben hätten Sie erwartet?

var a = 1;     // global variable
b = 2; 
function someFunction(){
  console.log(a);
  if (false){
    var a = 5; 
  } else {
    var b = 10;
  } 
  console.log(b);
}
someFunction ();
console.log(b);

Richtig sind die drei Ausgaben undefined, 10, 2. Warum ist das so? Die Variablen der äußeren Scopes werden, wie in anderen Sprachen auch, in die inneren Scopes vererbt, die Deklaration des lokalen a kommt nie zur Ausführung und dennoch überschreibt die lokale die globale Deklaration von a. Der Grund hierfür liegt darin, dass Variablen in JavaScript für die gesamte Funktion gültig sind. Da a jedoch nie ein konkreter Wert zugewiesen wurde, ist a zum Zeitpunkt der Ausgabe zwar innerhalb der Funktion deklariert, aber noch undefined.

Die Programmiersprache JavaScript kennt das Konzept der Namensräume nicht. Weil die Projekte, die inzwischen mit JavaScript realisiert werden, aber immer größer werden, wird ein alternativer Mechanismus zur Organisation des Programmcodes benötigt. Dessen Ziel soll sein, dass nicht alle Variablen und Eigenschaften in dem globalen Namensraum liegen, aber trotzdem von überall verwendet werden können. In JavaScript wird dieses Konzept „Modul“ genannt. Die Idee basiert darauf, eine Funktion als in sich geschlossenen Namensraum zu verwenden. Dieser kann Funktionen und Eigenschaften exportieren.

Im folgenden Beispiel soll das Modul mfi entstehen. Dazu wird zunächst eine Variable benötigt, die die Exports des Moduls aufnehmen kann. Diese soll hier mfi heißen. Anschließend wird eine Funktion erstellt, die sich selbst aufruft. Innerhalb dieser Funktion entsteht in JavaScript ein eigener Scope, der von außen nicht sichtbar ist. Die Variable mfi wird als Argument an die sich selbst aufrufende Funktion übergeben. Mithilfe dieser Variablen werden später die Exports aus diesem Modul nach außen realisiert.

var mfi = { };
(function (mfi) {
    // Programmcode für das Modul im eigenen Scope
})(mfi);

Innerhalb der selbstaufrufenden Funktion wird das gleiche Muster wiederholt. Der äußere Scope entspricht dabei dem Konstrukt eines Namensraums. Der innere Scope stellt eine Abgrenzung ähnlich einer „Klasse“ in C# dar. In Listing 5 soll ein Zähler (Counter) programmiert werden, mit einem C-tor sowie den Funktion Inc und Show.

var Counter = (function () {
    function Counter() {
      this.cnt = 0;
    }
    Counter.prototype.inc = function () {
      this.cnt++;
    };
    Counter.prototype.show = function () {
      console.log(this.cnt);
    };
    return Counter;
})();

Die Constructor-Funktion Counter wird von der sich selbst aufrufenden Funktion zurückgegeben und in einer Variablen innerhalb des Moduls für die weitere Verwendung zwischengespeichert. Im nächsten Schritt wird die zwischengespeicherte Constructor-Funktion Counter als zusätzliche Eigenschaft an das in mfi übergebene Objekt eingefügt. Auf diese Weise kann der Counter-Constructor aus dem Modul exportiert werden:

mfi.Counter = Counter;

Der vollständige Quellcode sieht dann aus wie in Listing 6.

var mfi;
(function (mfi) {
    var Counter = (function () {
        function Counter() {
          this.cnt = 0;
        }
        Counter.prototype.inc = function () {
          this.cnt++;
        };
        Counter.prototype.show = function () {
          console.log(this.cnt);
        };
        return Counter;
    })();
    mfi.Counter = Counter;
})(mfi || (mfi = {}));

// Anlegen und Verwenden eines Counters
var c1 = new mfi.Counter();
c1.inc();
c1.inc();
c1.inc();
c1.show();

Dieses Beispiel lässt sich mit jedem aktuellen Browser oder Node.js ausführen. Öffnen Sie dazu die JavaScript-Konsole (im Internet Explorer oder Chrome mit F12 unter den Entwicklerwerkzeugen zu finden). Geben Sie das Beispiel dann im mehrzeiligen Modus ein. Verwenden Sie in Chrome SHIFT + Enter anstelle von Enter und schließen Sie das Beispiel final mit Enter ab. Den Quellcode finden Sie hier.

Abb. 3: Ausführung des Beispiels im Chrome-Browser

Im nächsten Teil wird es um die Organisation von JavaScript-Code innerhalb eines Projekts mithilfe der Bibliothek RequireJS unter Verwendung des AMD Patterns (Asynchronous Modul Definition) und das Zusammenwirken der ASP.NET Web Optimization mit RequireJS gehen

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -