Neue Sprachfeatures für JavaScript im ECMAScript-6-Entwurf – Teil 2

Nicht nur Objektorientierung soll Teil von ECMAScript werden, auch Ergänzungen wie Promises sind geplant und lassen sich schon heute mit Transpilern einsetzen.

In Pocket speichern vorlesen Druckansicht 3 Kommentare lesen
Lesezeit: 11 Min.
Von
  • Martin Möllenbeck
  • Dr. Holger Schwichtenberg
Inhaltsverzeichnis

Nicht nur Objektorientierung soll demnächst Teil von ECMAScript werden, auch Sprachergänzungen wie Promises sind geplant und lassen sich schon heute mit Transpilern einsetzen.

Neben den im ersten Teil dieses Artikels vorgestellten Features im Bereich der Objektorientierung gehört die Unterstützung für Arrow-Funktionen und Promises für Callbacks zu den bedeutensten Erleichterungen, die die neue Version des ECMAScript-Standards bringt. Der Transpiler Traceur ermöglicht schon heute, ihn zum programmieren für alle Browser zu nutzen.

Im Browser wird beim Umgang mit dem DOM in der Regel mit Callback-Funktionen gearbeitet. Node.js hat das Prinzip der asynchronen Verarbeitung auf den Server übertragen. Da bei JavaScript mit this nicht das aktuelle Objekt gemeint ist, sondern der aktuelle Scope, ist beim Umgang mit Callbacks in bisherigen JavaScript-Versionen die aktuelle Instanz in einer temporären Variable zu speichern. Dadurch lässt sich im Callback dann auf die Instanz und somit auf die Variable zugreifen:

var badObj = {
name: "Holger",
handleMessage: function(msg, callback) {
callback(msg);
},
doIt: function() {
var that = this;

this.handleMessage("Hi ", function(msg) {
console.log(msg + that.name);
});
}
};

badObj.doIt();

Eine Lösung sind die mit ECMAScript 6 eingeführten sogenannten Arrow-Funktionen mit der Symbolfolge =>. Sie behalten die lexikalische Bindung von this bei. Dadurch ist es beim Umgang mit Callback-Funktionen nicht mehr notwendig, den Instanz-Kontext von this in einer lokalen Variable zu speichern, um sie dann in der Callback-Funktion verwenden zu können:

var goodObj = {
name: "Martin",

handleMessage: function(msg, callback) {
callback(msg);
},

doIt: function() {
// lange Schreibweise
this.handleMessage("Guten Tag ", (msg) => {
console.log(msg + this.name);
});

// kurze Schreibweise
this.handleMessage("Hi ", msg => console.log(msg + this.name));
}

};

goodObj.doIt();

Ein weiterer Vorteil ist die verkürzte Schreibweise. Sie erlaubt, dass man die runden Klammern um die Parameter und die geschweiften Klammern um den Funktionsbereich optional entfallen lassen kann.

Im Umgang mit den Arrow-Funktionen gibt es drei Dinge zu beachten:

  • Sie lassen sich nicht als Konstruktor-Funktion verwenden. Es ist somit nicht möglich, sie mit new aufzurufen.
  • Der this-Kontext lässt sich nicht durch bind() oder apply verändern.
  • Das Standard-Argument arguments kann nicht für alle Parameter verwendet werden. Als Alternative kommen nur die erwähnten Rest-Parameter in Frage.

Es gibt ein paar gute Gründe, warum derartige Einschränkungen existieren; der wichtigste ist das lexikalische Binden des this-Kontext. Des Weiteren können die JavaScript-Laufzeitumgebungen den Umstand, dass nur noch benannte Parameter verwendet werden dürfen, dazu nutzen, die Ausführung zu optimieren. Ein weiterer Vorteil ist, dass sich diese Funktionen nicht mehr als Konstruktor verwenden lassen und man die JavaScript-Laufzeitumgebung durch die strikten Regeln verbessern kann.

Promises haben das Ziel, eine lange Verschachtelung von Callback-Aufrufen zu verhindern und somit den JavaScript-Quellcode lesbarer und damit wartbarer zu gestalten. Bibliotheken wie jQuery und AngularJS implementieren sie derzeit unterschiedlich. Da mit Promises/A+ bereits eine Spezifikation exisitiert, war eine Aufnahme in den ECMAScript-Standard eine notwendige und sinnvolle Folge. Das untenstehende Beispiel zeigt eine Umsetzung eines typischen Programmablaufs unter Einsatz von Callbacks, wobei sich Entwicklern, die eine andere Sprache gelernt haben, der Ablauf nicht sofort erschließt.

function timeout(name, duration, callback) {
let doTimeout = function() {
console.log("doTimeout "+ name + " aufgerufen.");
callback(null);
};

setTimeout(doTimeout, duration);
}

try {
timeout("to_1", 100, (err) => {
if (err) {
console.log(err);
} else {
timeout("to_2", 200, (err) => {
if (err) {
console.log(err);
} else {
timeout("to_3", 300, (err) => {
if (err) {
console.log(err);
} else {
console.log("callbacks erledigt ;-)");
}
});
}
});
}
});
} catch (err) {
console.log("Fehler: " + err);
}

Betrachtet man hingegen den folgenden Ausschnitt, ist der Ablauf auf den ersten Blick verständlich.

function timeout(name, duration) {
return new Promise((resolve, reject) => {
let doTimeout = function() {
console.log("doTimeout "+ name + " aufgerufen.");
resolve();
};

setTimeout(doTimeout, duration);
})
}

let p1 = timeout("to_1", 100);
let p2 = timeout("to_2", 200);
let p3 = timeout("to_3", 300);

Promise.all([p1, p2, p3])
.then(() => {
console.log("promises erledigt ;-)");
})
.catch((err) => {
console.log(err);
});

Eine für viele lange fällige Ergänzung des Sprachumfangs von JavaScript ist die Einführung von Vorlagenzeichenketten (Template Strings). Bisher war es in JavaScript schwierig, die Übersicht zu behalten, wenn eine verkettete Zeichenfolge mit vielen festen und variablen Teilen zu erstellen war. Mit ${name} kann man in Zukunft innerhalb einer Zeichenkette Variablen ansprechen. In den geschweiften Klammern ist es zudem möglich, komplette Ausdrücke inklusive Funktionsaufrufe unterzubringen:

var moment = require('moment');

let world = 'Welt';

// Erstellen eines zusammengesetzten Text nach dem alten Muster.
let text_old = 'Altes hallo ' + world;
console.log(text_old);

// Erstellen eines zusammengesetzen Textes mit Hilfe von Template-Strings.
let text_new = `Neues hallo ${world}`;
console.log(text_new);

let x = 1;
let y = 2;
let rechenergebnis = `${x} + ${y} = ${x+y}`;
console.log(rechenergebnis);

function GetZeitpunkt() { return new Date(); }
let zeitpunktAusgabe = `Es ist jetzt:
${moment(GetZeitpunkt()).format("HH:MM")}`;
console.log(rechenergebnis);

Da seit einiger Zeit die Notwendigkeit besteht, Enterprise-Anwendungen beziehungsweise komplexere Bibliotheken für JavaScript zu entwickeln, entstanden einige Sprachen, um die Entwicklung von JavaScript zu vereinfachen. Die geläufigsten unter ihnen sind CoffeeScript, TypeScript und Dart. Sie stellen Abstraktionen von JavaScript dar, und ein Transpiler übersetzt ihre Syntax in JavaScript, sodass jeder Browser und jede andere auf das Ausführen von JavaScript ausgelegte Umgebung damit arbeiten kann. Von Google gibt es alternativ eine spezielle Version von Chrome, die in der Lage ist, Dart direkt auszuführen.

Wenn man die JavaScript-Abstraktionssprachen, im speziellen TypeScript, betrachtet, findet man viele Analogien zur kommenden JavaScript-Version. Microsoft ist maßgeblich an der Standardisierung von ECMAScript 6 beteiligt und hat TypeScript bewusst an ihr angelehnt. Auch bei Dart finden sich einige Ähnlichkeiten zum kommenden ECMAScript-Standard wieder, zum Beispiel Klassen und Vererbung. Die Verwandschaft zur CoffeeScript-Syntax ist nicht so groß, wenngleich Konzepte wie Spreads in ECMAScript 6 übernommen wurden.

Unter dem Namen Traceur gibt es bereits einen Transpiler (Compiler, der Quellcode von einer Programmiersprache in eine andere überführt) von ECMAScript-6- zu JavaScript-Code, der zu ECMAScript Version 3 oder 5 kompatibel ist. Die Entwickler von Traceur sind dabei bemüht, den Transpiler weiter voranzutreiben, sodass er bald den gesamten Sprachumfang unterstützt, soweit dies ohne Erweiterung der JavaScript-Laufzeitumgebung möglich ist. Alle Beispiele dieses Artikels sind – mit Ausnahme des WeakMap-Beispiels – mit Traceur übersetzt worden, was beweist, dass Traceur schon die wichtigsten ECMAScript 6-Features unterstützt.

Das folgende Codebeispiel zeigt, wie man eine ECMAScript-6-Datei mit Traceur übersetzt:

'use strict';

var traceur = require('traceur');
var path = require('path');

// Verwendung der als exprimentell gekennzeichneten Funktionen
traceur.options.experimental = true;

traceur.require.makeDefault(function(filename) {
// Ausnahmen definieren.
if (filename.indexOf("node_modules") != -1) {
return false;
}

// require aus node.js fuer alle anderen Dateien ueberschreiben
return true;
});

if (process.argv.length === 3) {
var filename = process.argv[2];

if (filename.indexOf("./") !== 0) {
filename = "./" + filename;
}

// Laden der JavaScript-Datei
require(filename);
} else {
console.log("ERROR: ");
console.log(" using " + path.basename(__filename) +
" <filename to use>.");
console.log("");
console.log(" sample: " + path.basename(__filename) +
"variables/sample_let.js");
process.exit();
}

Selbst bekannte Bibliotheksentwickler setzen ECMAScript 6 heute unter Verwendung von Traceur ein. Das bekannteste Beispiel dürfte Angular.js 2.0 sein. Diese Version soll vollständig in ECMAScript 6 implementiert werden. Da sie zusätzlich modularer aufgebaut sein wird, steht das AngularJS-Modul für Dependency Injection schon heute für den privaten Einsatz zur Verfügung. Damit zeigt sich, dass das neue ECMAScript bereits in der Praxis zum Einsatz kommt.

ECMAScript 6 enthält eine ganz Fülle von Neuerungen, sodass hier nicht alle im Detail abgehandelt werden können. Einige sollen jedoch an dieser Stelle noch Erwähnung finden:


  • Das Math-Objekt wird um neue Funktionen wie IsInteger() und IsNaN() erweitert.
  • Das Array-Objekt bietet nach der Aktualisierung zahlreiche neue Methoden wie entries(), keys(), values() und findIndex().
  • Das Destructuring bietet eine vereinfachte Syntax, um ein Array in seine Elemente aufzusplitten und eine Zuweisung an Variablen durchzuführen.
  • Proxies ermöglichen Wrapper, um bestehende Typen, zum Beispiel zu überwachen oder Logging und Profiling durchzuführen. Entwickler können aus einer Palette von Funktionen wählen, um gezielt Aufrufe des "gewrappten" Objekts an den Proxy umzuleiten.
  • Die Reflect API dient der Ermittlung von Meta-Daten eines Objekts, wie es in den gängigen Hochsprachen wie C# und Java bereits möglich ist.
  • Die Unterstützung von Unicode wird soweit ausgebaut, dass der globaler Einsatz einer Anwendung möglich wird. Die wichtigsten Erweiterungen sind Unicode-Literale in Strings und die Ergänzung der regulären Ausdrücke um ein Flag zum korrekten Umgang mit Unicode-Zeichen.
  • Das Schlüsselwort const bietet die Möglichkeit, Variablen als nicht veränderbar zu definieren. Eine erneute Zuweisung würde zu einem Laufzeit-/Compiler-Fehler führen.
  • Binary und Octal Literals ermöglichen binäre und oktale Zahlendarstellung im Quellcode.
  • Die meisten heutigen JavaScript-Laufzeitumgebungen sind nicht sehr robust beim Umgang mit tief geschachtelten rekursiven Funktionen, wie sie üblicherweise im mathematischen Umfeld nötig sind. ECMAScript 6 bietet für sogenannte "Tail Calls" eine verbesserte Verarbeitung, die tiefergehende Rekursionen ermöglicht.

ECMAScript 6 erweitert die Programmiersprache JavaScript um zahlreiche Sprachfeatures, die man von einer modernen Universalprogrammiersprache erwarten darf. Gerade Entwickler, die aus Sprachen wie C++, C# und Java zu JavaScript wechseln, werden sich dadurch in JavaScript wohler fühlen.

Sobald die Spezifikation von ECMAScript 6 Mitte 2015 den Status einer Empfehlung erreicht hat , liegt es an den Browserherstellern, zügig eine Umsetzung zu liefern. Einige Browser implementieren bereits ausgewählte ECMAScript-6-Features (siehe z.B. Firefox) – allerdings mit der Gefahr, dass sich der Standard noch ändern kann. Bis zur Verabschiedung des Standards kann der Transpiler Traceur eine Übergangslösung sein.

Aktuell haben TypeScript und Co den Vorteil, dass Entwicklungsumgebungen sie besser unterstützen als ECMAScript 6. Das wird sich allerdings bald ändern, sodass der direkte Einsatz eine echte Alternative wird.

Martin Möllenbeck und Dr. Holger Schwichtenberg
sind Entwicklungsleiter bei der 5minds IT-Solutions GmbH & Co. KG in Oberhausen. Sie entwickeln hochskalierbare Enterprise-Anwendungen mit .NET, JavaScript, Node.js und anderen zeitgemäßen Techniken.
(jul)