Die besten Tricks und Kniffe mit nativer JavaScript-Syntax

5 praktische Handgriffe für den JavaScript- und TypeScript-Alltag

Peter Kröner

©Shutterstock / kenary820

In JavaScript herrscht kein Mangel an syntaktischem Zucker, doch die schiere Menge der Möglichkeiten macht es manchmal schwer, überhaupt zu erkennen, was das eine oder das andere Feature überhaupt zu leisten im Stande ist – im Großen wie im Kleinen. Gerade auf der Ebene einfachster Ausdrücke und simpelster Einzeiler verbergen sich viele Möglichkeiten, die nicht jeder kennt!

Die Zeiten vor ECMAScript 6 bzw. ECMAScript 2015 müssen ausgesprochen finster gewesen sein. Zu diesem Eindruck komme ich jedenfalls, wenn ich mir die raketengleiche Entwicklung anschaue, die das Image von JavaScript seither genommen hat. War die Programmiersprache einst noch schwerpunktmäßig mit ihrer Aufgabe als Witzfigur befasst, ist sie mittlerweile unter ihresgleichen akzeptiert. Dabei hat sich eigentlich gar nicht so viel wirklich geändert. JavaScript ist immer noch eine primär imperative, dynamisch typisierte und ziemlich objektorientierte Sprache und seit der Witzfigur-Ära hat sich trotz vieler neuer Features an der Art und Weise, wie wir JS schreiben, nicht sonderlich viel getan. Nur ganz wenige Features haben die Standard-Herangehensweise an typische JS-Herausforderungen wirklich auf den Kopf gestellt.

So löst etwa der JS-Mainstream, seitdem es async/await gibt, asynchrone Aufgaben auf eine gänzlich neue Weise (imperativ, statt funktional mit Promises, oder chaotisch mit Callbacks). Selbst die Einführung von Klassen hat nicht wirklich einen vergleichbaren Effekt gehabt, denn wer „richtiges“ OOP wollte, konnte das auch zu Zeiten von ES3 haben – nur eben mit einer handgestrickten Implementierung des Klassenkonzepts.

Entscheidend sind auch die Kleinigkeiten

Was sagt es uns, wenn wir Kleinigkeiten wie eine einheitliche Klassensyntax oder Syntax-Bonbons wie Destructuring und Arrow Functions ebenso abfeiern wie die Einführung des eigentlich viel revolutionäreren async/await? Es sagt uns, dass die sogenannten Kleinigkeiten wichtig sind. Es macht einen Unterschied, dass es eine Standardlösung für Klassen-OOP gibt, denn es reduziert mentalen Overhead – es gibt einfach ein Problem weniger zu lösen. Und es macht einen Unterschied, wenn wir mit Destructuring Objekte zerlegen können, denn es macht Code kompakter – es gibt weniger Zeilen, die geistigen Arbeitsspeicher belegen. Kleinigkeiten können den Unterschied machen.


Und diese Kleinigkeiten müssen nicht immer offizielle Features sein. Wer hin und wieder auch mal in der physischen Welt Dinge (um)baut, weiß, dass tolle Werkzeuge und erlesene Materialien zwar das Fundament eines erfolgreichen Projekts bilden, aber eben auch nicht mehr. Man muss es auch verstehen, auf dieses Fundament aufzubauen. Und dabei sind es neben dem Big Picture oft die Kleinigkeiten, die rückblickend offensichtlich erscheinenden Handgriffe, die einen gewaltigen Unterschied am Fun-Factor ausmachen. Klar kommt man immer irgendwie zum Ziel, doch wenn einem der Meister, die Mama oder der Guru gezeigt haben, mit welchen simplen Kniffen man ein Problem besonders einfach und elegant löst, macht das Lösen gleich doppelt Laune.

In diesem Artikel stelle ich euch einige JavaScript-Handgriffe vor, die in etwa in der gleichen Liga spielen, wie das fachgerechte Aufklappen einer Mango oder Onkel Heinz‘ einfacher Kniff zum fummelfreien Einbau von Unterputz-Satellitendosen. Vielleicht kennt der eine oder andere Leser einige oder sogar alle diese Handgriffe schon, oder sie erscheinen offensichtlich. Aber vielleicht machen sie unseren JavaScript-Alltag auch punktuell sehr viel angenehmer und unseren Code eleganter.

1. Duplikate aus Listen werfen

Um in der Kreidezeit Listen zu de-duplizieren war es noch nötig, auf Library-Funktionen wie _.uniq() von lo-dash zurückzugreifen. Heutzutage kann diese Operation hingegen ein nativer Einzeiler sein:

const input = [ 0, 1, 2, 1, 0, 2, 3, 1 ];
const unique = [ ...new Set(input) ];
// Übrig bleibt in "unique" [ 0, 1, 2, 3 ]

Die Kombination von Sets mit Array-Spread sorgt dafür, dass aus dem Input-Array alle Doubletten ausgefiltert werden. Die gesamte Operation besteht aus zwei Teilschritten:

  1. new Set(input) konstruiert aus dem Input-Array ein Set, das per Definition jeden Wert nur einmal enthalten kann. Duplikate werden an dieser Stelle automatisch herausgeworfen, ohne dass die Reihenfolge verändert wird.
  2. Der Array-Spread überführt den Inhalt des Sets wieder in ein Array. Da unser Input-Objekt ein Array war, wollen wir vermutlich für den Output auch wieder ein Array und kein Set haben – das Set ist reines Mittel zu Deduplizierungs-Zwecken.

Wichtig ist, dass der Set-Input kein Array sein muss, sondern jedes beliebige Objekt sein kann, das das Iterable-Protokoll implementiert. Kurz gesagt: Alles was von einer for-of-Schleife verarbeitet werden kann, kann zu einem Set gemacht und damit von Duplikaten befreit werden. Dazu gehören neben Arrays auch NodeLists, DomStringLists und die Iteratoren, die die Methoden values() und keys() für Maps liefern.

Der einzige Haken hierbei ist, dass die Gleichheit von Werten von Sets durch den SameValueZero-Algorithmus festgestellt wird. Dieser funktioniert fast wie ein Vergleich mit ===, mit der Ausnahme, dass NaN als gleich zu NaN betrachtet wird, was normalerweise nicht der Fall ist. Das bedeutet aber auch, dass bei Referenzen zwei unterschiedliche, aber inhaltlich äquivalente Objekte nicht als Duplikate erkannt werden, was unter vielen Umständen wünschenswert ist. In solchen Fällen führt kein Weg an einem Detailvergleich der Objekte oder einem Vergleich von Objekt-Hashes vorbei.

2. Arrays löschen

Auch wenn der Webentwicklungs-Zeitgeist im Moment Immutability favorisiert, ist es manchmal doch ganz praktisch, den Inhalt von Datenstrukturen zu verändern. So ist es bei den jüngeren Standard-Objekten Map und Set sehr einfach, alle Daten zu löschen: die clear()-Methode aufrufen und fertig! Eine solche Methode gibt es für die althergebrachten Arrays nicht direkt, aber eine recht einfache Alternative:

 myArray.length = 0;

Die length-Eigenschaft von Arrays spiegelt immer die Anzahl der im Array enthaltenen Felder wider, doch dass diese Eigenschaft auch gesetzt werden kann, ist vielen JavaScript-Entwicklern gar nicht so richtig klar. Dabei ist dies nicht nur die beste Möglichkeit, ein Array komplett zu leeren, auch zum zurechtstutzen bietet sie sich an:

let myArray = [ "a", "b", "c" ];
myArray.length = 2;
// übrig bleibt [ "a", "b" ]

Umgekehrt kann gezielte Manipulation der length-Eigenschaft auch zusätzliche Felder in einem Array anlegen:

let myArray = [ "a", "b", "c" ];
myArray.length = 5;

for (let value of myArray) {
  console.log(value);
}
// > "a"
// > "b"
// > "c"
// > undefined
// > undefined

Das length-Feld von Arrays ist im Übrigen auch ein schönes Beispiel dafür, dass es Integers in JavaScript irgendwie doch schon immer gab. Während der Typ von length immer schon „number“ war, war es stets unmöglich, eine length zu setzen, die keine ganze, positive Zahl war:

let x = [];
x.length = 23.42
> RangeError: Invalid array length
x.length = -42
> RangeError: Invalid array length

Andererseits darf der Wert für length auch kein echter Integer sein, denn ein BigInt mag die length-Eigenschaft auch nicht haben:

let x = [];
x.length = 42n; // n-Suffix = BigInt
> Uncaught TypeError: Cannot convert a BigInt value to a number

Die length-Eigenschaft eines JavaScript-Arrays ist also vieles auf einmal: Sie ist eine Number (aber auch nicht ganz), ein bisschen ein Integer (aber auch nicht wirklich) und vor allem ein wunderbarer Ersatz für eine clear()-Methode.

3. Benannte Callbacks für setTimeout und Co

Wenn es gilt, eine Funktion in regelmäßigen Abständen auszuführen, bedient sich der geneigte JS-Entwickler normalerweise der setInterval()-Methode. Diese ist aber nur solange die richtige Wahl, wie die auszuführende Funktion schnell und synchron bleibt. Sollte die auszuführende Funktion aufwändig werden oder ihrerseits mit asynchronen Callbacks hantieren, ist es besser, rekursiv setTimeout() aufzurufen, in etwa so:

setTimeout( function () {
  doStuff();
  setTimeout(???, 1000); // was tun?
}, 1000);

Aber was kommt dahin, wo im obigen Codeschnipsel die Fragezeichen stehen? Die Antwort ist so einfach wie irritierend: Der Name der Funktion, den wir tatsächlich ganz einfach an die Callback-Funktion vergeben können:

setTimeout( function myFunc () {
  doStuff();
  setTimeout(myFunc, 1000);
}, 1000);

Dies ist eine der extrem seltenen Stellen in JavaScript, an denen named function expressions zu etwas gut sind. Herkömmliches JS verwendet meist nur zwei Arten der Funktionsdefinition, nämlich normale Funktionsdeklarationen und anonyme Funktionsausdrücke:

// Funktionsdeklaration
function foo () { return 42; }

// Anonymer Funktionsausdruck
let foo = function () { return 42; };

Der entscheidende Unterschied ist hierbei nicht, ob nach dem function-Keyword der Name „foo“ folgt, sondern dass sich die zweite Funktion auf der rechten Seite einer Zuweisung befindet, und damit im Prinzip ein Input für etwas anderes ist. Für Funktionsdeklarationen, die für sich allein stehen (erste Funktionsdefinition), ist ein Name verpflichtend, denn sonst gäbe es keine Möglichkeit, die Funktion zu referenzieren. Das bedeutet aber nicht, dass Funktionsausdrücke keinen Namen haben dürften! Normalerweise brauchen sie keinen, denn sie werden entweder in Variablen oder als Callbacks in andere Funktionen gestopft, aber im Prinzip können Sie einen Namen haben:

let foo = function bar () { return 42; };

Der Effekt einer solchen Konstruktion ist eine Funktion, die

  1. außerhalb ihrer selbst in der Variablen „foo“ zu finden ist.
  2. innerhalb ihrer selbst sowohl unter „foo“ als auch unter „bar“ zu erreichen ist.
  3. eine name-Eigenschaft mit dem Inhalt „bar“ hat.

Nützlich sind solche Funktionen eigentlich nur dann, wenn sie als Callback-Funktionen eingesetzt werden sollen, die auch jenseits ihrer Rolle als Parameter referenzierbar bleiben sollen, so wie es bei rekursivem Einsatz von setTimeout() der Fall ist. Ein anderes Beispiel wäre ein Retry-Feature:

fetch("/some/data").then( function handleResponse (response) {
  if (!response.ok) {
    fetch("/some/data").then(handleResponse); // erneuter Versuch!
  } else {
    somethingElse(response);
  }
});

Named Function Expressions sind eines der besonderen Features, die nur mit per function-Keyword definierten Funktionen umgesetzt werden können. Arrow Functions zwar sind auch Funktionsausdrücke, aber ihre Syntax erlaubt nicht die Vergabe eines Namens – stattdessen übernimmt ihre name-Eigenschaft den Namen der Variablen, der sie zugewiesen wurden. Vorausgesetzt, eine solche Variable existiert und die Funktion ist nicht einfach ein Callback.

4. Transformieren und Filtern in einem Schritt

Wer gern im funktionalen Stil programmiert, transformiert Arrays unter zuhilfename der map()-Methode und filtert sie mittels filter()-Methode. Aber was tun, wenn Transformieren und Filtern auf einmal passieren soll, zum Beispiel wenn ein großes Array nicht zweimal durchlaufen werden soll? Kein Problem mit flatMap():

let numbers = [ 0, 1, 2, 3, 4, 5, 6 ];
let doubledEvens = numbers.flatMap( (n) => n % 2 === 0 ? [ n * 2 ] : [] );
// ergibt [ 0, 4, 8, 12 ]

Der eigentliche Zweck von flatMap() ist, ein Array im Rahmen einer Transformation aufzublasen. Während die Map-Methode jeden Wert im Input-Array zu genau einem anderen Wert im Output-Array transformiert, kann flatMap() aus jedem Wert im Input-Array eine beliebige Menge von Werte im Output-Array generieren – je einem pro Element im aus der Transformationsfunktion herausgegebenen Array. Und diese beliebige Menge von Werten kann 0 sein!

Zwei Operationen in einem Schritt zusammenzufassen, kann unter verschiedenen Umständen sinnvoll sein. Abgesehen davon, dass es aus schlichten Erwägungen der Code-Ökonomie hilfreich ist, triviale Berechnungen (wie im obigen Beispiel) zu kombinieren, kann auch Performance eine Rolle spielen. Es ist kein Drama, ein Array erst zu filtern und dann zu transformieren…

let numbers = [ 0, 1, 2, 3, 4, 5, 6 ];
let doubledEvens = numbers
  .filter( (n) => n % 2 === 0 )
  .map( (n) => n * 2 );
// ergibt auch [ 0, 4, 8, 12 ]

… wogegen es schon problematischer ist, wenn erst transformiert und dann ausgefiltert werden muss! Sofern es möglich ist, vor einer Operation festzustellen, ob diese durchgeführt werden muss, sollte dies auch getan werden – ansonsten fallen überflüssige Berechnungen an, bei denen wir auch nicht erwarten dürfen, dass uns die Magie der JS-Engines eine performante Variante herbei-optimiert.

5. Objekt-Destructuring für Arrays

In erster Näherung sind JavaScript-Arrays nichts weiter als Objekte mit numerischen Keys. Die Unterschiede zwischen einem Array und einen Fake-Array sind marginal:

let fakeArray = {
  "0": "Hello",
  "1": "World",
  "length": 2
};
let x = fakeArray[1];
// x === "World"

Der Zugriff per „Index“ funktioniert (der als Zahl angegebene Index wird automatisch stringifiziert und passt dann auf die entsprechende Objekt-Property), mit einer Vanilla-For-Schleife. So ist das Fake-Array ohne Probleme durchzuiterieren und mit moderatem Einsatz könnte der geneigte Entwickler auch Methoden wie push() und map() an das Fake-Array anbauen oder auch Iterations-Protokolle implementieren.

Das Iterations-Protokoll wäre nötig, um für das Fake-Array Array-Destructuring zu ermöglichen. Die praktische Extraktions-Syntax mit den eckigen Klammern…

let array = [ "A", "B", "C", "D", "E", "F", "G", "H", "I" ];
let [ first, second ] = array;
// first === "A", second === "B"

… ist nämlich gar kein array-spezifisches Feature, sondern ist ein Feature des Iterable-Protokolls. Daher kann das so genannte „Array-Destructuring“ mit allem verwendet werden, was mit For-Of-Schleifen kompatibel ist und nicht nur mit Arrays. Für normale Objekte gibt es hingegen keine besonderen Anforderungen, weswegen das normale Objekt-Destructuring immer und mit jeder Art von Objekt funktioniert:

let obj = { foo: 23, bar: 42, baz: 1337 };
let { foo, baz } = obj;
// foo === 23, baz === 1337

Das normale Objekt-Destructuring bietet den Vorteil, dass wir ungewollte Felder einfach ignorieren können – wenn wir „bar“ nicht brauchen, führen wir es einfach nicht auf. Bei Array-Destructuring ist das Auslassen von Feldern etwas unschöner:

let array = [ "A", "B", "C", "D", "E", "F", "G", "H", "I" ];
let [ first, , , fourth ] = array;
// first === "A", fourth === "D"

Da Array-Destructuring das Iteratable-Protokoll nutzt und das zweite und das dritte Element nun mal leider vor dem vierten Element stehen, müssen wir das zweite und das dritte Element durch zusätzliche Kommata auslassen. Spätestens wenn wir an das erste, das siebte und das neunte Element wollen, wird der Kommasalat unübersichtlich – es sei denn wir zweckentfremden einfach Objekt-Destructuring für Arrays!

let array = [ "A", "B", "C", "D", "E", "F", "G", "H", "I" ];
let { 0: foo, 6: bar, 8: baz } = array;
// foo === "A", bar === "G", baz === "I"

Wie eingangs erwähnt sind Arrays wenig mehr als Objekte mit numerischen Keys und wir wissen auch, dass Objekte ohne besondere Vorkehrungen mit Objekt-Destructuring traktiert werden können. Daraus folgt, dass wir Objekt-Destructuring auf Arrays anwenden und dabei die Indizes als Feldnamen verwenden können! Zwar sind rein numerische Variablennamen nicht erlaubt, aber das ist mit der Umbenennungs-Syntax von Objekt-Destructuring kein Problem. Die Syntax { a: x } = o bedeutet so viel wie „extrahiere aus o den Wert von Feld a und schreibe ihn in die Variable x“, was sich wunderbar auf die numerischen Index-Keys unseres Array-Objekt-Destructurings anwenden lässt. Besonders nützlich ist dieser Kniff, wenn wir ihn auf Arrays anwenden, aus denen wir mehr als nur ein paar Felder extrahieren wollen. Mit Array-Objekt-Destructuring können wir z. B. neben Werten auch die length-Eigenschaft in einer einzigen, bequemen Deklaration extrahieren:

let array = [ "A", "B", "C", "D", "E", "F", "G", "H", "I" ];
let { 6: bar, 8: baz, length } = array;
// bar === "G", baz === "I", length === 9

Der wahre Oberhammer ist die Kombination von Array-Objekt-Destructuring mit RegExpArrays, wie sie von der exec()-Methode regulärer Ausdrücke ausgespuckt werden. Neben Werten an numerischen Indizies enthalten diese neben dem Input-String auch den Index der aktuellen Fundstelle im Input. Alles Informationen, die sich aus dem sehr konfus aufgebauten RegExpArray mittels Array-Objekt-Destructuring bequem extrahieren lassen!

let { 0: match, index, input } = /bar/.exec("foobar");
// match === "foo", index === 3, input === "foobar"

Wenn es nur um normale Werte in halbwegs sinnvoll genutzten Arrays geht, ist Array-Objekt-Destructuring oft nicht nötig, aber für die Fälle, in denen neben normalen Feldern auch noch die length (oder im Fall von RegExpArrays noch sehr viel mehr) extrahiert werden muss, ist dieser JavaScript-Handgriff einer der praktischeren.

Verwandte Themen:

Geschrieben von
Peter Kröner
Peter Kröner
Der bekannte Webtechnologieexperte und Buchautor Peter Kröner forscht über die Webstandards von morgen. Was er über HTML5, CSS3 und neue JavaScript-Standards herausfindet, schreibt er in Bücher und Blogposts oder vermittelt es in Seminaren, Workshops und Vorträgen im ganzen Land weiter. Zuvor war er als selbstständiger Webdesigner und Frontend-Entwickler tätig.
Kommentare

Hinterlasse einen Kommentar

avatar
4000
  Subscribe  
Benachrichtige mich zu: