ES6/ES7/ES8 Features

ES6(ECMAScript 2015), ES7(ECMAScript 2016) und ES8(ECMAScript 2017) bringen einen Haufen Vorteile gegenüber normalem Javascript. In diesem Artikel möchte ich die praktischsten vorstellen.

ES6

ES6 wurde im Jahre 2015 finalisiert und war das bedeutendste Upgrade der JavaScript Programmiersprache. Dabei wurden viele Konzepte aus anderen Programmiersprachen übernommen und Funktionen aus externen Bibliotheken in den Sprachkern integriert. Während alte Applikationen und Skript auch unter ES6 weiter funktionierten, ergaben sich viele neue Möglichkeiten, mit weniger Aufwand schnelleren und übersichtlicheren Code zu generieren.

Neue Methoden

Diversen Standardobjekten wurden neue Methoden verpasst. Speziell beim String-Datentyp kann man sich nun in vielen Stellen das Gehempel mit regulären Ausdrücken für Standardabfragen sparen. Die Beispiele sollten selbsterklärend sein. Zu den SafeIntegers empfehle ich den Artikel in der Mozilla MDN.

// String
"Hallo".startsWith("He");
"Features".endsWith("es");
"ha".repeat(2); // ==> "haha"
"abcdef".includes("bcd");

// Number
Number.EPSILON;
Number.MAX_SAFE_INTEGER; // INT_MAX bei c
Number.MIN_SAFE_INTEGER; // INT_MIN bei c

Number.isNan(x);
Number.isFinite(x);
Number.isInteger(x);
Number.isSafeInteger(x);
Number.parseFloat(x);
Number.parseInt(x);

let a = { b: 2};
Object.extend(a, {c: 5, d:1}, {c: 42}); // äquivalent $.extend()
// ==> a = { b: 2, c: 42, d: 1}

let

Bei ES5 waren nur Variablendeklarationen mit dem var-Statement möglich und waren in der gesamten aktuellen Funktion sichtbar (function scope). Speziell bei for-Schleifen oder innerhalb von Blöcken konnte dies zu Problemen führen und machte die Programmierung unübersichtlich. Ab ES6 sind mit dem let-Schlüsselwort ganz normale Blockvariablen (beispielsweise wie in C) möglich.

// Klassisch
function abc()
{
  var b = 25;
  // Variable a gibt es auch nach der For-Schleife noch!
  for(var a=0; a<10; a++) 
  {
    // Temporäre Variable anlegen.
    // Überschreibt leider den Wert von Variable b von oben.
    var b = a*a; 
    console.log(b);
  }

  console.log(b); // Hier wird 81 ausgegeben.
}
// ES6
function abc()
{
  var b = 25;
  // Variable a gibt nur innerhalb der For-Schleife.
  for(let a=0; a<10; a++) 
  {
    // Temporäre Variable anlegen.
    // Diese existiert nur in diesem Block und hat 
    // nichts mit b von oben zu tun.
    let b = a*a; 
    console.log(b);
  }

  console.log(b); // Hier wird 25 ausgegeben.
}

const / freeze / seal

Bis dato fehlte ein const-Ausdruck für Variablen in JavaScript. Dies wurde nun zusammen mit freeze und seal ergänzt.

// ES6

// Konstantes Objekt erzeugen.
const a = { b: 25 };
a = 42; // Erzeugt einen TypeError

// Die Werte eines konstanten Objekts können sich 
// jedoch noch ändern.
a.b = 42; // funktioniert

// Gegen weitere Änderung kann man das Objekt einfrieren
Object.freeze(a);

// Nun sind weder Änderungen an Struktur noch an den
// Werten möglich
a.b = 43; // TypeError
a.newParam = 10; // TypeError

// Möchte man nur die Struktur verriegeln, Werteänderungen
// aber weiter zulassen, so gibt es die seal-Methode.
var b = {x: 5};
Object.seal(b);

Array-Funktionen

Die Methode Array.from(x) kann aus so ziemlich allem, was indizes und eine Längenangabe hat, ein Array erzeugen.

Array.from("abc"); // ==> Array["a","b","c"]

let arrayObject = {
  0: 'null',
  1: 'eins',
  2: 'zwei',
  'length': 3
};

Array.from(arrayObject);
// Array ["null", "eins", "zwei"]

Zudem hat das Array-Objekt ein paar neue Methoden bekommen.

['k','l','m'].keys()
// ==> Array Iterator {}

[...['k','l','m'].keys()]
// ==> Array [0, 1, 2]

Array.from(['k','l','m'].entries())
// ==> [[0, "k"], [1, "l"], [2, "m"]]

Array.from("abc"); // ==> Array["a","b","c"]

let arrayObject = {
  0: 'null',
  1: 'eins',
  2: 'zwei',
  'length': 3
};

Array.from(arrayObject);
// Array ["null", "eins", "zwei"]

[1, 5, 50 ,15].find(x => x>25);
// 50

[1, 5, 50 ,15].findIndex(x => x>25);
// 2

Arrow-Funktionen

Man kann sich bei Funktionen einen haufen Schreibarbeit sparen, speziell bei Callbacks.

// ES5
var f = function(x){
  return x+1;
}

// ES6
let f = x => x + 1;

// Mit 2 Parametern:
let g = (x, y) => x + y;

// Ohne Parameter
let g = () => 42;

Arrow-Funktionen haben einen weiteren Vorteil. Sie verwenden den this-Kontext von der übergeordneten Funktion. Konstrukte wie .bind(this), that oder self entfallen damit!

Methoden in Objekten

Bei Methoden innerhalb von Objekten kann nun das Schlüsselwort function weggelassen werden.

// Klassisch
var object1 = {
  a: function(x){
    return x+25;
  }
};

// ES6
let object1 = {
  a(x) {
    return x+25;
  }
};

super

Von abgeleiteten Klassen (entweder TypeScript oder per abc.prototype) kann nun die übergeordnete Methode aufgerufen werden.

// ES6
let object3 = {
  toString() {
    return "Ich bin ein " + super.toString();
  }
};

console.log(object3.toString()); 
// ==> Ich bin ein [object Object]

Berechnete properties

ES6 unterstützt zum ersten Mal computed properties. Damit lassen sich ähnliche Felder oder Methoden definieren.

// ES6
let object4 = {
  ['field' + ind]: 'this is field'+ind;

  // Logarithmus von 'value' zur Basis 'base'
  ['log' +  base](value) {
    return Math.log(value) / Math.log(base);
  }
};

console.log(object4.field100); // ==> this is field 100
console.log(object4.log2(8)); // ==> 3 (Logarithmus zur Basis 2 von 8)

Klassen und Vererbung

ES6 unterstützt Klassen und deren Vererbung. Das ganze sieht ziemlich ähnlich zu PHP aus.

// ES6
class Jedi{
  constructor()= {
    this.forceIsDark = false;
  }
  toString() {
    return (this.forceIsDark ? 'Join' : 'Fear is the path to')+
      ' the dark side';
  }
};

class Sith extends Jedi {
  constructor() {
    super();
    this.forceIsDark = true;
  }
}

let yoda = new Jedi();
let maul = new Sith();

console.log(maul instanceof Sith); // ==> true
console.log(maul instanceof Jedi); // ==> true

console.log(yoda.toString()); 
// ==> Fear is the path to the dark side

console.log(maul.toString()); 
// ==> Join the dark side

Außerdem unterstützt ES6 statische Felder in Klassen.

// ES6
class Foo{
  static f(x){
    return x+42;
  }
}

console.log(Foo.f());

Eine Sache gibt es bei Klassen jedoch zu beachten: Im Gegensatz zu Funktionen sind Klassen erst ab der Zeile verfügbar, wo sie deklariert wurden. Sie werden also nicht wie Funktionen nach oben gezogen (hoisting).

Getter/Setter in Klassen

Man kennts schon von c#, nun geht das ganze auch in ES6.

// ES6
class Foo{
  constructor(){
    this.r = 40
  }

  get radius() { return this.r; }
  get durchmesser() { return this.r*2; }

  set radius(val){
    this.r = val;
  }
}

console.log(Foo.f());

Iteratoren

Über ein Array oder Objekt zu iterieren war bei ES5 ein ziemlicher Krampf. ES6 bietet nun endlich entsprechende Mechaniken.

// ES5
var arr = ['1','2','3'];
for(var i in arr)
  if(arr.hasOwnProperty(i))
    console.log(i);

// ES6
for(let i of ['1','2','3'])
  console.log(i);

In diesem Zusammenhang gibt es noch ein paar weitere merkwürdige Iteratoren.

// Spread operator
[..."abcd"]; // ==> Array["a","b","c","d"]

// Destructure assignment
[a,b] = "xy"; // ==> a="x" , b="y"

Maps

Dies ist das Äquivalent zu HashMap Strukturen in C#. Während ES5 das ganze mit Objekten emuliert hat, geht es bei ES6 nun nativ.

var m = new Map([
  [1, 'erster'],
  [{}, 'zweiter']
]);

m.set("AA", "dritter");

m.delete(1);

console.log(m.size); // ==> 2

m.forEach((val, key) ==> console.log(val));

m.clear();

Gleicheit bei den Schlüsseln (die übrigens sogar Funktionen sein können) wird mit dem === Operator geprüft. Daher ist {} !== {} !!!!

Sets

Im Prinzip wie Maps, jedoch nur mit value und ohne Key.

var s = new Set(["blau", "gelb"]);
s.add("lila");

s.has("blau"); // ==> true

[...s]; 
// ==> Array ["blau", "gelb", "lila" ]

Generatoren

Man kann nun Generatorfunktionen erzeugen. Mittels yield kehrt man zurück zum Aufrufenden, der interne Zustand wird aber beibehalten und beim nächsten Aufruf macht man nach dem yield weiter.

function *genFour() {
  yield 1;
  yield 2;
  yield 3;
  return 4;
}

// Verwendung als Iterator
let four = genFour();

four.next(); // ==> { value: 1, done: false}
four.next(); // ==> { value: 2, done: false}
four.next(); // ==> { value: 3, done: false}
four.next(); // ==> { value: 4, done: true}
four.next(); // ==> { value: undefined , done: true}

// Verwendung als Iterable
[...genFour()];
// ==> Array [1, 2, 3, 4]

Promises

Um dem Callback-Hell zu entgehen, haben jQuery und andere Bibliotheken vor einigen Jahren Promises eingeführt. Diese sind jetzt auch nativ in der Sprache verfügbar.

requestFile()
.then(function(result){ return readFile(result); }
.then(function(result2){ .... return anotherresult; }
.catch(function(error){ ...do something....; }

Sofern eine Funktion ein thenable-Object zurückliefert, wird der nachfolgende then-Block erst aufgerufen, wenn ein Ergebnis aus dem thenable-Object vorliegt. Oben im Beispiel sieht man das an dem readFile-Aufruf. Ein Promise rezeugt man relativv einfach:

new Promise(function(resolve, reject) {
  if(checkSomething()){
    resolve(cargoValue);
  } else {
    reject(cargoValue);
  }
});

Dabei kann ein Promise 3 Zustände annehmen: Pending, Resolved(fulfilled) oder Rejected.

BTW: Wenn man einen ganzen Haufen von Promises hat, die erfüllt werden müssen, bevor es weitergehen kann, sollte man die Methode Promise.all([]) verwenden, die ein Array von Promises annimmt und erst den then-Teil ausführt, wenn alle Promises den Status Pending verlassen haben.

Destructuring

…oder wie nehme ich Strukturen und Objekte wieder auseinander…?

Man stelle sich vor, man hat eine Struktur (Objekt) und möchte daraus ein paar Werte extrahieren.

var a = {x:1, y:2, z:3};

// ES5
var x = a.x;  // Gleicher Name
var n = a.z;  // Unterschiedlicher Name
console.log(x); // ==> 1
console.log(n); // ==> 3

// ES6
let {x:x, z:n} = a; // x -> x, z -> n
console.log(x); // ==> 1
console.log(n); // ==> 3

Bisher recht unspektakulär. Interessant wird das ganze aber, wenn einzelne properties nicht im Objekt vorkommen. Dann kann man nämlich Default-Werte definieren.

// ES6
let a = {x:1, y:2};

let {x:x = 40, z:n = 42 } = a; // x -> x, z -> n
console.log(x); // ==> 1
console.log(n); // ==> 42

Die Default-Werte werden lazy ausgewertet, das heißt, ihr Wert wird erst bestimmt, wenn er wirklich benötigt wird. Da man bei den Default-Werten auch Funktionen angeben kann, wird die Funktion erst aufgerufen, wenn der Default-Wert benötigt wird.

Es gibt beim Destructuring noch einen Haufen weitere Möglichkeiten, beispielsweise mit Arrays oder der Extraktion verbleibender Werte, das würde aber an dieser Stelle den Rahmen sprengen. Außerdem hat https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Operators/Destrukturierende_Zuweisung eine sehr gute Übersicht über die Möglichkeiten.

ES7

Neue Methoden

Diversen Standardobjekten wurden neue Methoden verpasst.

// String
"5".padLeft(4); // ==> "   5"
"1".padRight(2); // ==> "1 "
"3".padLeft(2,"0"); // ==> "03"
let numbers = [2, 3, 5, 9, 10];

// ES6
if(numbers.indexOf(5) !== -1)
  console.log("5 ist drin");

// ES7
if(numbers.includes(5))
  console.log("5 ist drin");

In einer früheren Version dieser Seite stand oben numbers.contain(5), was natürlich falsch ist. Vielen Dank an L3P3 für die Fehlermeldung!

Exponent-Operator

Seit ES7 gibt es einen Exponential-Operator.

// ES7
let n = 2 ** 10; // ==> 1024

ES8

async und await

Nachdem C# diese beiden Schlüsselwörter eingeführt hat, war es nur eine Frage der Zeit, wann das ganze in andere Programmiersprachen rüberschwappt. Man kann nun Methoden oder Funktionen als async deklarieren. Diese Funktionen müssen dann ein Promise zurückgeben.

Mit dem await-Schlüsselwort kann man nun darauf warten, dass das Promise fertig wird.

// ES7
async function getData() { ... }


(async function() {
  await loadData();
  console.log("Data successfully loaded!");
}());

Sonstiges

ES8 hat auch noch ein Event-Modell mit Observern und Subscribern, die Infos darüber (und über sinnvolle Anwendungsfälle) halten sich jedoch derzeit noch in Grenzen.
Zudem wurden neue Primitiven eingeführt und die Möglichkeit, eigene Einheiten (z.B. Längeneinheiten) zu definieren.

Eine weitere Möglichkeit besteht in der Operatoren-Überladung. Speziell, wenn man mit Vektoren und Matritzen arbeitet, könnte man damit vermutlich ganz nette Dinge bauen.

Bei ES8 muss man sich jedoch im Klaren darüber sein, dass es sich um bisher experimentelle Features handelt. Eine abschließende Übersicht bietet https://developer.mozilla.org/en-US/docs/Web/JavaScript/New_in_JavaScript/ECMAScript_Next_support_in_Mozilla.