EcmaScript 2015, Babel und TypeScript

Für Sie interessant:

HTML 5 / CSS 3 / JavaScript

Nach dem Besuch des Seminars HTML 5 / CSS 3 / JavaScript haben Sie Kenntnisse von den Möglichkeiten der clientseitigen Technologien einer modernen Anwendung.

 

Gerade ist der zweite „Release Candidate“ der EcmaScript 6 Spezifikation erschienen, der nächsten Version von JavaScript. Der Name wurde in EcmaScript 2015 geändert, woraus sich schließen lässt, dass es in Zukunft jedes Jahr eine neue Version der Sprache geben soll, so wie es auch schon von 1997 – 1999 zu den Anfängen von EcmaScript war. Mit dem Release ist im Sommer dieses Jahres zu rechnen. Die Browserhersteller sind schon fleißig dabei, den neuen Standard zu implementieren. Daher kann es nicht schaden, schon jetzt einen Blick auf die neuen Features der Sprache zu wagen.

Arrow Functions

Ein sehr kleines, aber nützliches neues Feature von EcmaScript 2015 sind Arrow Functions. Dies ist eine verkürzte Schreibweise von Funktionen und ist schon länger in anderen Sprachen wie C# und mittlerweile auch Java zu finden.

var wordsService = {

  excludedWords: ['one', 'four'],

  filterWords(words) {
    return words.filter(word => this.excludedWords.indexOf(word) === -1);
  }
};

console.log(wordsService.filterWords(['one', 'two', 'three', 'four', 'five']));

Hier wird der wordsService genutzt, um aus einem Array mit Wörtern bestimmte Wörter herauszufiltern und diese dann in der Konsole auszugeben. In EcmaScript 5 sah der entsprechende Code noch wie folgt aus:

var wordsService = {
  
  excludedWords: ['one', 'four'],
  
  filterWords: function(words) {
    var self = this;
    return words.filter(function(word) { 
return self.excludedWords.indexOf(word) === -1;  
    });
  }
};

console.log(wordsService.filterWords(['one', 'two', 'three', 'four', 'five']));

Neben der verkürzten Schreibweise gibt es aber auch semantisch einen kleinen Unterschied. Während normale Funktionen mit this immer einen eigenen Kontext besitzen, haben Arrow Functions keinen eigenen Kontext, daher können wir uns in EcmaScript 2015 den Umweg über die self-Variable sparen und über this direkt auf das exlcudedWords-Attribut vom wordsService zugreifen.

Promises

Möchte man per JavaScript mit anderen Systemen interagieren, dann geschieht das meistens über asynchrone Schnittstellen, d. h. man übergibt bei der Interaktion eine Funktion, welche am Ende der Interaktion mit dem Ergebnis aufgerufen wird (Callback).

api.getJSON('/api/customers', 
  customers => storage.storeCustomers(customers,
    customers => customers.forEach(customer => console.log(customer.name + ' wurde gespeichert.')),
    error => console.log('Fehler: ' + error)
  ), 
  error => console.log('Fehler: ' + error));

Hier wurden zwei Funktionen an api.getJSON übergeben, eine für den Fall einer erfolgreichen Interaktion und eine für den Fall einer nicht erfolgreichen Interaktion. Bei erfolgreichem Abfragen der Kunden werden diese mit storage.storeCustomers, einer weiteren asynchronen Schnittstelle, weiter verarbeitet. Unter Umständen kann diese Art zu programmieren eine sehr tiefe Verschachtelung von Callbacks verursachen. Abhilfe schaffen da schon länger verschiedene Bibliotheken (z. B. auch jQuery), welche mit Promises oder ähnlichen Konzepten arbeiten. Nativ werden Promises ab EcmaScript 2015 unterstützt. Die Idee ist nicht eine Callback-Funktion zu übergeben, sondern ein Promise, d. h. ein Versprechen zurückzugeben, dass irgendwann ein Ergebnis geliefert wird. Diese Promises können dann weiter verarbeitet werden.

api.getJSON('/api/customers')
  .then(storage.storeCustomers)
  .then(customers => customers.forEach(customer => console.log(customer.name + ' wurde gespeichert.')))
  .catch(error => console.log('Fehler: ' + error));

Promises können wie folgt erzeugt werden:

var promise = new Promise((resolve, reject) => {

// resolve und reject sind Funktionen die zu einem beliebigen Zeitpunkt entweder mit dem Ergebnis oder dem Fehler aufgerufen werden können

});

Generators

Eins der interessantesten Features ist eine neue Art von Funktion, ein Generator. Ein Generator gibt immer ein Objekt zurück, welches eine next-Funktion besitzt. Wird diese next-Funktion aufgerufen, wird der Code im Generator bis zum ersten Auftreten des yield-Schlüsselwortes ausgeführt. Mit yield kann ein Wert zurückgegeben werden, dieser Wert ist dann Teil des Rückgabewertes der next-Funktion. Bei dem nächsten Aufruf der next-Funktion, wird der Code im Generator dann bis zum Auftreten des nächsten yield-Schlüsselwortes ausgeführt. Wurde der Generator komplett durchlaufen, wird dies ebenfalls über den Rückgabewert der next-Funktion mitgeteilt. Die Funktionsweise entspricht in etwa der yield return-Anweisung in C#.

function*getWords() {
  yield 'one';
  yield 'two';
  yield 'three';
  yield 'four';
}

var words = getWords();

console.log(words.next()); // { value: 'one', done: false }

for(var word of words) {
  console.log(word);
}

Analog zu der for … in-Schleife, welche Arrays durchläuft, kann man mit der for … of-Schleife die Liste eines Generators durchlaufen. Im Prinzip wird nichts anderes gemacht, als die next-Funktion aufgerufen, bis ein Wert zurückgegeben wird, bei dem das done-Attribute true ist. Ein Generator liefert also eine Aufzählung von Elementen. Was diese Aufzählung von Elementen von einem Array unterscheidet, ist, dass ihre Werte erst beim Durchlaufen errechnet werden und nicht von vorneherein feststehen. Dies kann besonders von Vorteil sein, wenn man verschiedene Operationen auf eine Liste von Elementen Anwenden möchte, wie man es z. B. mit LINQ in C# machen kann.

var query = products
  .where(p => p.price > 100)
  .orderby('priority')
  .take(10)
  .select(p => p.name);
  
for(var name of query) {
  console.log(name);
}

Klassen

Mit der neuen class-Syntax bekommt die eher funktionale Sprache einen Hauch von objektorientierter Programmierung, auch wenn hinter der neuen Syntax immer noch die schon jetzt verfügbare und auf Prototypen basierende Semantik steckt. Die neue Syntax dürfte vor allem Entwickler, die Erfahrung mit objektorientierten Programmiersprachen haben, anlocken. Mit dem constructor-Schlüsselwort wird der Konstruktor der Klasse definiert, das extends-Schlüsselwort ermöglicht von einer anderen Klasse abzuleiten und mit super() wird der Konstruktor dieser Klasse aufgerufen.

class HeaderComponent extends Component {
  
  constructor(options) {
    super(options.name);
    this.text = options.text;
    this.position = options.position;
  }
  
  render(context) {
    context.font = '30px Arial';
    context.fillText(this.text, this.position.x, this.position.y);
  }
}

Module

Ein großes Problem von JavaScript war schon immer, dass sich der Code im Großen schlecht strukturieren ließ. So wurden z. B. alle Abhängigkeiten (Bibliotheken usw.) in den globalen Geltungsbereich geladen, was wiederum oft zu Konflikten geführt hat. Mit der Zeit sind Modulsysteme entstanden, um diesem Problem entgegenzutreten. Die AMD-Spezifikation (Asynchronous Module Definition) erlaubt z. B. den Code in Module zu unterteilen, deren Abhängigkeiten explizit zu definieren und diese dann asynchron zu laden. Eine der bekanntesten Implementierungen von AMD ist RequireJS. CommonJS ist eine weitere Spezifikation, welche ein synchrones Modulsystem definiert. Diese Spezifikation wird z. B. von NodeJS implementiert um Module synchron laden zu können. Mit EcmaScript 2015 bekommt JavaScript nun ein natives Modulsystem, welches die besten Eigenschaften von CommonJS und AMD vereint. Alles, was in ein Modul gehört, wird in einer Datei zusammengefasst und was exportiert werden soll, wird mit dem export-Schlüsselwort exportiert. Mit dem default-Schlüsselwort kann ein Standardwert exportiert werden.

// MyComponent.js
export default class Component {
  constructor(options) {
    this.options = options;
  }
  render(element) {
    // ...
  }
}

export function Factory(options) {
  return new Component(options);
}

Um das Modul aus „MyComponent.js“ in einem anderen Modul zu importieren, nutzt man das import-Schlüsselwort. Es kann entweder nur der Standardwert importiert werden, z. B. mit import MyComponent from ‚MyComponent‘;, oder es kann explizit definiert werden, welche Bestandteile importiert werden sollen:

// App.js
import { Component as MyComponent, Factory } from 'MyComponent';

var myComponent = new MyComponent({ name: 'myComponent' });
var myComponent2 = Factory({ name: 'myComponent2' });

Transpiler

Wer sich fragt, warum er sich schon heute mit EcmaScript 2015 beschäftigen sollte, obwohl der Standard noch nicht von den Browserherstellern implementiert wurde und es wahrscheinlich noch lange dauern wird, bis auch jeder einen Browser mit Unterstützung von EcmaScript 2015 nutzt, der sollte einen Blick auf die verschiedenen Transpiler mit Unterstützung von EcmaScript 2015 werfen. Transpiler kompilieren Quellcode zu Quellcode, dabei kann z. B. Quellcode mit Features aus EcmaScript 2015 in JavaScript-Code gewandelt werden, welcher auch heute schon in jedem Browser ausgeführt werden kann.

Babel

Einer der bekanntesten und umfangreichsten Transpiler für EcmaScript 2015 ist Babel. Wer sehen möchte, wie Babel arbeitet, der kann sich hier das Beispiel zu den Arrow Functions anschauen. Neben den Features aus EcmaScript 2015 werden auch experimentelle Features angeboten, diese sollten aber besser nicht produktiv eingesetzt werden. Das folgende Beispiel zeigt Code mit Features aus EcmaScript 2015, welcher von Babel zu EcmaScript 5 konformen Code kompiliert werden kann.

var fibonacci = {
  [Symbol.iterator]: function*() {
    var pre = 0, cur = 1;
    for (;;) {
      var temp = pre;
      pre = cur;
      cur += temp;
      yield cur;
    }
  }
}

for (var n of fibonacci) {
  if (n > 1000) {
    break;
  }
  console.log(n);
}

TypeScript

Einen weiteren interessanten Transpiler liefert Microsoft mit TypeScript. TypeScript ist eine Obermenge von JavaScript und erweitert es um statische Typisierung. Der TypeScript-Kompiler gibt standardkonformen und verständlichen JavaScript-Code aus, der heute in allen Browsern lauffähig ist. TypeScript ermöglicht durch die statische Typisierung zum einen viel besseres Tooling und soll so zum anderen die Entwicklung von großen JavaScript-Anwendungen vereinfachen. Neben der statischen Typisierung werden auch Features aus EcmaScript 2015 unterstützt. Die nächste Version von TypeScript soll EcmaScript 2015 fast komplett unterstützen. Da mittlerweile auch Google für die Entwicklung seines Angular-Frameworks auf TypeScript setzt und Facebook mit flow auf dieselbe Syntax wie TypeScript um statische Typisierung in JavaScript umzusetzen, stehen die Chancen gut, dass JavaScript in Zukunft optionale statische Typisierung erhält. Microsoft und Google haben zumindest angekündigt, gemeinsam einen Vorschlag dafür einzureichen.

function getMax(list : T[], isGreater : (a : T, b: T) => boolean) {
	var max = list[0];
	for(var i = 1; i < list.length; i++) {
		if(isGreater(list[i], max)) {
			max = list[i];
		}
	}
	return max;
}

var max = getMax([5, 6, 2, 15, 9, 10, 3, 8, -2], (a, b) => a < b);

Das Beispiel zeigt eine generische Funktion, welche das größte Element in einer Liste ermittelt. Als Parameter wird zum einen die Liste erwartet und zum anderen eine Funktion, welche entscheidet, ob ein Element größer ist als ein anderes. Daraus generiert der TypeScript-Kompiler folgenden JavaScript-Code:

function getMax(list, isGreater) {
    var max = list[0];
    for (var i = 1; i < list.length; i++) {
        if (isGreater(list[i], max)) {
            max = list[i];
        }
    }
    return max;
}
var max = getMax([5, 6, 2, 15, 9, 10, 3, 8, -2], function (a, b) { return a < b; });

Da TypeScript eine Obermenge von JavaScript ist, kann man mit TypeScript auch jede JavaScript-Bibliothek nutzen. Wer dabei die fehlende statische Typisierung der Schnittstellen vermisst, der findet mit DefinitelyTyped auf GitHub ein Projekt, welches statische Typisierungen für die gängigsten JavaScript-Bibliotheken sammelt. Die Typdefinitionen für externe JavaScript-Bibliotheken beginnen mit dem declare-Schlüsselwort und werden in Dateien mit der Endung „.d.ts“ festgehalten. Hätten wir die getMax-Funktion aus dem Bespiel als JavaScript-Code bekommen, hätten wir sie also nachträglich mit einer statisch typisierten Schnittstelle versehen können:

declare function getMax(list : T[], isGreater : (a : T, b: T) => boolean) : T

Neben Babel und TypeScript gibt es noch andere Transpiler, die Features von EcmaScript 2015 unterstützen, wie z. B. Traceur von Google oder JSTransform von Facebook. Einen Überblick darüber, welche Browser und Transpiler welche Features unterstützen, ist hier zu finden. Einige Transpiler unterstützen schon Features von EcmaScript 7, z. B. asynchrone Funktionen. Diese Features sind aber noch weit davon entfernt, standardisiert zu sein und sollten daher noch nicht produktiv eingesetzt werden. Nachdem die Weiterentwicklung von JavaScript lange auf sich warten lassen hat, nimmt diese nun volle Fahrt auf. Vor allem die vielen verschiedenen Transpiler tragen zu dem schnellen Entwicklungsprozess der Sprache bei, da sie helfen schon früh in Erfahrung zu bringen, ob es sinnvoll ist ein neues Feature zu integrieren.