Imperative Programmierung Teil 1
Wie die meisten bekannten Sprachen lässt sich mit JavaScript imperativ programmieren. Im imperativen Programmierstil schreibt man ein Programm in einer Abfolge von Anweisungen, die den Zustand des Programms verändern. Der imperative Programmierstil wurde bereits in den 50er-Jahren geprägt. Auf ihm basieren die Programmiersprachen Fortran, Pascal und C, die zu den Vorgängern von JavaScript zählen.
Ausdrücke und Operatoren
JavaScript kennt die aus C und Java bekannten Ausdrücke und Operatoren wie Multiplikation (*), Divison (/) und Modulo (%). Addition und Subtraktion lassen sich wie gewohnt, inklusive bekannter Kurzschreibweisen wie ++ oder –, durchführen. Selbst Zeichenketten lassen sich mit dem Plus-Operator verketten.
Bekannte logische Operatoren wie UND (&&), ODER (||) und NOT (!) können in JavaScript ebenfalls wie gewohnt verwendet werden. Auch Bit-Operatoren wie das Verschieben von Bits oder die Definition von Bitmasken bergen keine Überraschungen.
Da JavaScript dynamisch typisiert ist, lassen sich diese Operatoren auch zum Casten von Werten oder Variablen verwenden. Da beispielsweise ausschließlich Zahlen multipliziert werden können, lassen sich Strings durch eine Multiplikation in eine Zahl wandeln:
typeof ("42"*1) // number
Vergleiche
Einen etwas anderen Weg als die meisten gebäuchlichen Sprachen geht JavaScript bei den Vergleichsoperatoren. Gleichheit und Ungleichheit können entweder streng oder normal geprüft werden. Eine normale Prüfung (==, !=) vergleicht nur Werte, Typen werden zum Vergleich dynamisch zur Laufzeit angepasst. Eine strenge Typprüfung (===, !==) vergleicht außerdem den Typ der Operanden. Da die normale Prüfung Überraschungen in sich bergen kann, bevorzugen viele Entwickler die strenge Prüfung.
42 == “42” // true
42 === "42" // false
Bei Ausdrücken, die zu wahr oder falsch valuiert werden gibt es in JavaScript einige Besonderheiten zu beachten. Ein Ausdruck ist dann unwahr, wenn er folgenden Wert hat:
- false
- null
- undefined
- ” (leerer String)
- 0 (die Zahl Null) oder NaN (der Zahlenwert „Not a Number“)
Ein Ausdruck ist wahr für alle Werte, die nicht unwahr sind.
Achtung: Dies gilt auch für den String „false“ oder den numerischen Wert -1!
Die Eigenschaft, dass ein Ausdruck wahr für alle Werte ist, die nicht unwahr sind, lässt sich nutzen, um eine Zahl oder einen String in einen booleschen Wert zu wandeln:
!!0 // false
!!1 // true
Neben Gleichheit und Ungleichheit kann auch auf Größe (< , <=, >, >=) geprüft werden. Wichtig ist, dass JavaScript die Größe meist lexikalisch und ohne strenge Typprüfung prüft. Dies kann zu merkwürdigen Ergebnissen führen, wenn man nicht bedenkt, dass von einer lexikalischen Sortierung ausgegangen wird. So ist die Zeichenkette „42“ größer als die Zeichenkette „411“, die Zahl 42 ist allerdings kleiner als die Zeichenkette „411“.
Variablen
Variablen und Konstanten werden in der Regel Werte (Literale) oder Objekte bzw. Funktionen zugewiesen. Variablen deklariert man mit der var-Anweisung.
var x;
Variablen können bereits in der Deklaration einen Initialwert bekommen, sie lassen sich also gleichzeitig deklarieren und definieren. Solange einer Variablen kein Wert zugewiesen wurde, ist ihr Wert gleich dem besonderen Wert „null“. Ihr Typ wird zu „undefined“ ausgewertet.
var x;
x == null; // true
typeof x; // undefined
x = 5;
x; // 5
typeof x; // number
In einer var-Anweisung können mehr als eine Variable definiert werden. Diese Variablen werden durch Kommas getrennt.
var x, y, z;
In vielen Coding-Conventions wird verlangt, dass man alle in einem Gültigkeitsbereich verwendeten Variablen zu Beginn des Gültigkeitsbereichs deklariert. Daher ist es nützlich, dass man mehrere Variablen in einer einzigen Zeile deklarieren kann.
Blöcke und Gültigkeit von Variablen
Code lässt sich in JavaScript in Blöcken strukturieren. Blöcke werden wie schon in C und auch in Java in geschweifte Klammern eingeschlossen. Allerdings haben Blöcke weit weniger Bedeutung in JavaScript als in C oder Java.
{
var x = 10;
print(x); // 10
}
JavaScript behandelt nämlich den Gültigkeitsbereich von Variablen anders als C oder Java. Variablen, die in einem Block deklariert wurden, sind nicht nur in diesem Block, sondern in der ganzen Funktion, in der sie deklariert wurden, sichtbar.
{
var x = 10;
print(x); // 10
}
print(x); // 10
Gültigkeitsbereich für Variablen
Achtung: Anders als in vielen anderen Programmiersprachen, wie Java und C, wird durch einen Block kein neuer Gültigkeitsbereich für Variablen definiert. Das heißt, auch Variablen, die in einem Block definiert wurden, sind außerhalb des Blocks nach ihrer Deklaration sichtbar.
Wenn eine var-Anweisung innerhalb einer Funktion verwendet wird, so wird eine lokale Variable erzeugt. Diese Variable ist nur innerhalb der Funktion sichtbar. Von außen kann nicht auf sie zugegriffen werden. Außerhalb einer Funktion erzeugt die var-Anweisung jedoch eine globale Variable. Globale Variablen sind in der Regel nicht erwünscht, da sie den globalen Namesnraum verschmutzen.
Eine Variable kann auch ohne die var-Anweisung erzeugt werden, dann aber stets als globale Variable, was zu einer „Verschmutzung“ des globalen Namensraums führt. In ECMAScript 5th Edition ist im Strict Mode daher die Definition einer Variablen ohne die var-Anweisung nicht mehr erlaubt.
Verwende die Var-Anweisung
Eine Variable sollte stets durch die Var-Anweisung definiert werden.
Außer über die var-Anweisung sollen sich lokale Variablen in der kommenden ECMAScript 6th Edition auch mit der let-Anweisung deklarieren und definieren lassen. Die let-Anweisung soll eindeutig zwischen lokalen und globalen Variablen unterscheiden. Zudem hat die let-Anweisung einen Blockgültigkeitsbereich und keinen Funktionsgültigkeitsbereich. Sie kommt daher der Variablendefinition anderer Sprachen wie Java sehr viel näher.
Neben Variablen lassen sich in der kommenden ECMAScript 6th Edition auch Konstanten defi-nieren. Werte werden Konstanten bereits während der Deklaration zugewiesen, sie werden direkt definiert. Diese Werte lassen sich später nicht mehr ändern.
const PI = 3.14159265;
Wenn man allerdings versucht, den Wert einer Konstanten später zu verändern, dann ignorieren die meisten Laufzeitumgebungen dies einfach, ohne einen Fehler zu verursachen. Dies kann zu schwer zu findenden Bugs im Code führen.
Zahlen
Zahlen, number-Literale, werden als 64-Bit-Fließkommazahlen (64 Bit entsprechen 8 Byte) gespeichert. Dies entspricht dem Typ double in Java. In Zukunft könnte sich dies durchaus ändern. Von der Genauigkeit her bedeutet dies, dass natürliche Zahlen bis zu einer Größe von 15 Zeichen (9E15) als genau angesehen werden können. Bruchzahlen sind nur so genau wie möglich anzusehen. Dies muss beispielsweise bei Währungsberechnungen berücksichtig werden.
0.05 + 0.01 = 0.060000000000000005
Es gibt keine Unterscheidung zwischen natürlichen Zahlen und Bruchzahlen.
const C1 = 299792458;
const C2 = 2.99792458E8; // C1 == C2
var saldo = -768;
Zahlen lassen sich auch hexadezimal ausdrücken.
print(0xFF); // liefert 255
NaN
NaN (Not a Number) ist keine Zahl. Über die Funktion isNaN lässt sich überprüfen, ob ein Wert eine Zahl ist. NaN lässt sich nicht vergleichen, auch nicht mit sich selbst.
var notANumber1 = NaN;
print(notANumber1 == NaN); // liefert false
print(isNaN("Hello")); // liefert true
print(isNaN("3.27E6")); // liefert false
Infinity
Eine besondere Zahl ist Infinity (Unendlich). Unendlich sind alle Zahlen, die größer sind als der Wertebereich, den eine Zahl annehmen kann. Vergleicht man zwei infinite Zahlen miteinander, so ist das Ergebnis wahr.
var infinite = 2E308;
print(infinite); // liefert Infinity
print(Infinity == infinite); // liefert true
print(infinite == infinite * 2); // liefert true
Formatierung und Konvertierung
Mit Zahlen möchte man nicht nur rechnen, sondern man möchte die Zahlen auch formatiert als String zur Anzeige bringen. Zur Formatierung von Zahlen gibt es verschiedene Methoden wie num-ber.toFixed, number.toPrecission, number.toString oder number.toExponential. Einzelheiten dazu lassen sich dem Anhang des Buchs entnehmen. Um Strings (zurück) in Zahlen zu wandeln, gibt es in JavaScript zwei globale Funktionen: parseInt und parseFloat. Auch diese werden im Anhang näher be-schrieben.
Zeichenketten
Zeichenketten werden im String-Literal gespeichert.
typeof "Hello World" // String
Als JavaScript entwickelt wurde, war Unicode noch ein 16-Bit-Zeichensatz. Darum ist ein Zeichen in JavaScript 16 Bit breit. Es gibt kein Literal für ein einzelnes Zeichen (Character). Ein einzelnes Zeichen lässt sich durch eine ein Zeichen lange Zeichenkette ausdrücken.
Zeichenketten werden durch einfache oder doppelte Anführungszeichen umschlossen. Als Escape-Zeichen wird der Backslash „\“ verwendet.
var myString = "Hello World\nJetzt kommt eine neue Zeile";
Unicode-Zeichen lassen sich direkt über ihren Unicode-Wert ausdrücken.
print("¢" === "\u00A2"); // true
Die Länge eines Strings lässt sich über die length-Eigenschaft ermitteln. Dies ist keine Methode, sondern eine echtes Attribut.
print("Hello World".length); //11
Wie alle Literale sind Strings unveränderlich. Methoden, die auf einem String aufgerufen werden, verändern diesen also nicht, sondern geben einen neuen String zurück.
Die Methoden des String-Literals entsprechen den Methoden des String-Objekts, da intern das Literal zu einem Objekt wird, wenn eine Methode aufgerufen wird.
Boolesche Werte
Das Boolean-Literal hat zwei Werte: true und false.
typeof true // boolean
Arrays
Ein Array ist eine lineare Liste von Werten, bei denen über Positionsangaben auf einzelne Werte zugegriffen werden kann. Man kann in einem Array Werte unterschiedlicher Typen mischen.
var planets = ["Merkur", "Venus", "Erde", "Mars"];
print(planets[2]); // Erde
Array-Literal und Array-Objekt
Array-Literale erzeugen Array-Objekte. Das heißt, eine durch ein Array-Literal definierte Variable ist nicht vom Typ „array“, sondern vom Type „object“.
typeof planets // object
Dies ist ein typisches Problem bei der Verwendung von Arrays. Oft wird in einer Methode als Eingabeparameter ein Array erwartet, man bekommt aber ein Objekt (z.B. einen String) oder es wird ein Objekt (z.B. ein String) erwartet und man bekommt ein Array. Es obliegt dem Programmierer zu prüfen, ob er ein Array oder ein anderes Objekt bekommen hat.
Der typeof-Operator ist hier nicht hilfreich, da dieser sowohl bei einem String-Objekt als auch bei einem Array schlicht object zurückliefert. Zu prüfen, ob das übergebene Objekt die length-Eigenschaft hat, funktioniert leider auch nicht, denn Array-ähnliche Objekte wie Strings haben auch diese Eigenschaft. Man muss also zusätzlich prüfen, ob der Konstruktor des übergebenen Objekts der Array-Konstruktor ist:
function isArray(value) {
return (value && // value ist defined
typeof value === "object" && // value ist ein Objekt
value.constructor === Array) // Konstruktor ist Array
}
print(isArray(["eins", "zwei", "drei"])); // true
print (isArray("Hello")); // false
toArray-Hilfsmethode
Manchmal ist es auch egal, ob das übergebene Objekt ein Array oder ein String ist. Array-ähnliche Objekte wie Strings lassen sich mit einer Hilfsmethode, die sich u.a. in der jQuery-Library von John Resig befindet, in Arrays wandeln. Inhalte von Arrays selbst werden durch diese Hilfsmethode nicht verändert:
function toArray(value) {
return Array().slice.call(value, 0);
}
print(toArray("Hello")); // H,e,l,l,o
print(toArray(47)); // empty
print(toArray(["eins", "zwei", "drei"])); // eins,zwei,drei
Reguläre Ausdrücke
Reguläre Ausdrücke in JavaScript entsprechen im Wesentlichen den regulären Ausdrücken in Perl. Reguläre Ausdrücke lassen sich in JavaScript direkt als Literal angeben. Sie werden durch einen Slash eingeleitet und auch beendet.
var regexp = /\((\d*)\)/;
Allerdings gibt es keinen Basistyp regexp. Ein solches Literal erzeugt eine Funktion.
typeof /\((\d*)\)/; // function
Da reguläre Ausdrücke eine eigene Sprache bilden, können sie sehr komplex werden. Darum er-folgt an dieser Stelle nur eine kurze Einführung.
Reguläre Ausdrücke werden verwendet, um Zeichenketten zu validieren bzw. diese Zeichenketten auf Muster abzubilden. Um eine Zeichenkette zu valideren, muss man ein Muster (Pattern) definieren, das einem Suchkriterium entspricht. Dieses Suchkriterium kann man dann auf eine Zeichenkette anwenden.
Muster werden aus String-Literalen und Metazeichen gebildet. Der oben bereits definierte reguläre Ausdruck sucht in einer Telefonnummer die Vorwahl. Um zu testen, ob eine Zeichenkette eine Vorwahl enthält, lässt sich die string.search-Methode verwenden:
var regexp = /\([\d\w]*\)/;
print("Telefon: (040) 55555".search(regexp)); //9
Der reguläre Ausdruck sucht eine öffnende Klammer „\(“ gefolgt von beliebig vielen Zahlen „\d“ oder Leerzeichen „\w“ ausgezeichnet durch „*“, gefolgt einer schließenden Klammer „/)“.
„\d“ und „\w“ sind Metazeichen.
Auf die Zeichenkette “Telefon: (040) 55555″ erfolgt eine Abbildung des regulären Ausdrucks. Die string.search-Methode liefert eine positive Position zurück, an der das gesuchte Muster beginnt. Also enthält die Zeichenkette eine Vorwahl.
Um zu überprüfen, ob eine Zeichenkette ausschließlich eine Vorwahl enthält, muss der reguläre Ausdruck durch weitere Metazeichen ergänzt werden.
var vorwahl = /^\([\d\w]*\)$/;
print("Telefon: (040) 55555".search(vorwahl)); //-1
print("(404)".search(vorwahl)); // 0
Das Metazeichen „^“ drückt aus, dass vom Anfang, das Metazeichen „$“, dass bis zum Ende der Zeichenkette der reguläre Ausdruck abgebildet werden muss.
Neben Metazeichen gibt es Flags, die einen regulären Ausdruck unabhängig von Groß-/Kleinschreibung machen oder ihm Mitteilen, dass über Zeilengrenzen hinweg abgebildet werden soll. Diese Flags werden an das Ende des regulären Ausdrucks angehängt.
Kommentare
Code lässt sich durch Kommentare leichter verständlich machen. Es gibt zwei Arten von Kommen-taren: Blockkommentare und Zeilenkommentare.
Blockkommentare
Blockkommentare können über Zeilen hinweg laufen. Sie werden durch /* */ eingeschlossen. Diese Zeichenfolge wurde gewählt, da sie in gewöhnlichem Programmcode nur sehr selten vorkommt. Trotzdem ist diese Zeichenfolge nicht gänzlich ausgeschlossen.
/*
var matches = /\d*/.match("1234");
*/
Dieses Listing führt zu einem Syntaxfehler, da in regulären Ausdrücken (siehe oben!) durchaus diese Zeichenfolge vorkommen kann.
$ v8 blockcomments.js
blockcomments.js:2: SyntaxError: Unexpected token .
var matches = /\d*/.match(1234);
^
SyntaxError: Unexpected token .
Zeilenkommentare
Zeilenkommentare gelten nur bis zum nächsten Zeilenumbruch. Sie beginnen mit einem Double-Slash //. Einige Autoren empfehlen, lediglich Zeilenkommentare zu verwenden.
// Folgender Codeblock demonstriert den Zeilenkommentar.
// Es wird der Wert 5 ausgegeben, da das Inkrement auskommentiert wurde.
var a = 5;
// a++;
print(a);
Für reine Dokumentationszwecke sind jedoch Blockkommentare lesbarer.
/*
Folgender Codeblock demonstriert den Zeilenkommentar.
Es wird der Wert 5 ausgegeben, da das Inkrement auskommentiert wurde.
*/
var a = 5;
// a++;
print(a);
Tokens und Whitespaces
Der Quelltext eines JavaScript-Programms wird in eine Reihe von Tokens, Kommentaren und Whitespaces gewandelt. Der Interpreter wertet diese von links nach rechts aus.
Whitespaces, also Leerzeichen, Tabs und Zeilenumbrüche, werden verwendet, um einzelne Tokens voneinander zu trennen. Darüber hinaus haben sie keine Bedeutung. Man setzt sie ein, um Code zu strukturieren.
Eine Anweisung kann man mit einem Semikolon abschließen. Dieses Semikolon ist allerdings optional. In Fällen, in denen Semikola fehlen, wird stets die längst mögliche Sequenz von Zeichen interpretiert. Daher führt folgendes Skript, wenn man es nicht in einer REPL verwendet, zu einer unerwarteten Ausgabe: 1.625
var a = 5 / 8
-8 + 9
print(a) // 1.625
Die Zeilen (1) und (2) werden von der Laufzeitumgebung nämlich zu einer einzigen Zeile zusam-mengefasst und dann wie folgt interpretiert.
var a = 5 / 8 - 8 + 9;
print(a);
Es gibt zwar einige Programmierer, die einen möglichst minimalen Stil pflegen und auf alle überflüssigen Zeichen verzichten. Trotzdem empfiehlt es sich, auch wenn Semikola optional sind, diese zur besseren Lesbarkeit des Codes und zur Vermeidung von Fehlern zu verwenden.