Das Örtchen RSS-Feed
Kategorie
Kategorie: Blog
Buttons & Statistiken
Neueste Kommentare

JavaScript: Higher-Order-Funktionen oder der Verzicht auf Schleifen

Spätestens seitdem ES6 (bzw. ECMAScript 2015) in allen modernen Browsern Einzug gehalten hat, kann man mit den sogenannten Higher-Order-Funktionen viele Dinge lösen, die man vorher nur umständlich und schlecht lesbar mit Schleifen umgesetzt hat.

Wir haben beispielsweise ein 2-dimensionales Array mit dem Namen, der Adresse und dem Geschlecht mehrerer Personen.

 let data = [
  ["Jim", "Panse", "Musterstrasse 99", "Musterdorf", "m"],
  ["Rosa", "Schlüpfer", "Mustergasse 69", "Musterhausen", "w"],
  ["Max", "Muster", "Musterstrasse 1", "Musterstadt", "m"],
  ["Maxine", "Muster", "Musterstrasse 1", "Musterstadt", "w"]
]; 

Anstatt var verwende ich hier übrigens das mit ES6 neu eingeführte Schlüsselwort let für meine Variable data. Der primäre Unterschied zwischen var und let besteht im Scope bzw. Gültigkeitsbereich der so initialisierten Variable. Für die folgenden Beispiele ist dieser Unterschied jedoch nicht weiter relevant. Ich werde bei allen Beispielen (auch für ältere Vorgehensweisen) let verwenden, um konsistenten Code zu zeigen.

Beispiel 1:

Wie hat man nun typischerweise vor ES6 ein Array erzeugt, welches 1-dimensional ist und lediglich den Namen (also Vor- und Nachname) als String enthält?

 let names = [];
for (let i = 0; i < data.length; i++) {
  names.push(data[i][0] + " " + data[i][1]);
} 

Wirklich lesbar ist der Code nicht. Mit Array.prototype.map() geht dies viel besser:

 let names = data.map((person) => {
  return person[0] + " " + person[0];
}); 

Map gehört, wie schon geschrieben, zu den Higher-Order-Funktionen. Dies sind Funktionen, die als Parameter wiederum Funktionen übergeben bekommen. Bei der Angabe in den äußeren runden Klammern handelt es sich also um die Definition einer Funktion. Diese wurde als Pfeilfunktion notiert, was im Gegensatz zu den vor ES6 üblichen Funktionsausdrücken mit dem function-Schlüsselwort eine wesentlich kürzere Syntax erlaubt.

Die Pfeilfunktion drückt aus, was mit jedem einzelnen Wert des data-Arrays beim Mapping geschehen soll. Der Parameter ist natürlich beliebig benennbar, sollte aber ausdrücken, was ein Wert des Arrays darstellt (hier eine Person).

Unsere Pfeilfunktion ist bereits sehr kurz, lässt sich aber durch zwei optionale Kurzschreibweisen noch weiter reduzieren:

 let names = data.map(person => person[0] + " " + person[0]); 

Zunächst einmal benötigt man bei der Definition einer Pfeilfunktion mit genau einem Parameter keine runden Klammern um den Parameter. Diese sind jedoch verpflichtend, wenn die Funktion keinen Parameter oder mehr als einen Parameter hat.

Außerdem kann man die geschweiften Klammern und das return-Schlüsselwort weglassen, wenn die Funktion nur eine einzige Anweisung enthält. In einem solchen Fall gibt man den Rückgabewert nicht mehr explizit an. Stattdessen bildet die einzige erlaubte Anweisung dann gleichzeitig auch den Rückgabewert.

Beispiel 2:

Gehen wir nun davon aus, daß wir lediglich die Damen mit allen Angaben als Array haben wollen. Typischerweise wurde dies vor ES6 ebenfalls mit einer Schleife gelöst.

 let females = [];
for (let i = 0; i < data.length; i++) {
  if (data[i][4] === "w") {
    females.push(data[i]);
  }
} 

Dies geht mit Higher-Order-Funktionen wesentlich kürzer und lesbarer:

 let females = data.filter(person => person[4] === "w"); 

Auch hier erhält die Funktion Array.prototype.filter() wieder eine Pfeilfunktion als Parameter. Diese Funktion drückt aus, welche Bedingung ein Wert des data-Arrays erfüllen muss, um im gefilterten Array enthalten zu sein. Wir sehen zudem sofort, daß females ein gefiltertes Array auf Basis der Personen des data-Arrays ist.

Beispiel 3:

Gehen wir nun davon aus, daß wir im gefilterten Array alle Personen haben wollen, bei denen jeder Wert des Unterarrays mit einem m beginnt. Ja, doofes Beispiel aber mir fiel gerade nichts einfacheres ein, wofür kein neues Array benötigt würde.

 let mPersons = [];
for (let i = 0; i < data.length; i++) {
  let check = true;
  for (let j = 0; j < data[i].length; j++) {
    if (data[i][j].toLowerCase().indexOf("m") !== 0) {
      check = false;
    }
  }
  if (check) {
    mPersons.push(data[i]);
  }
} 

Vor ES6 hat man hierfür typischerweise zwei verschachtelte Schleifen verwendet. Alternativ könnte man auch auf die innere Schleife verzichten und stattdessen für jeden Wert des Subarrays eine Bedingung in der if-Anweisung notieren. Letzteres wäre jedoch sehr schlechter Stil, da das Subarray fünf Werte hat und wir somit fünf Bedingungen kombinieren müssten.

Mit Higher-Order-Funktionen sparen wir uns sehr viel Code und verbessern gleichzeitig wieder die Lesbarkeit:

 let mPersons = data.filter(person =>
  person.every(value => value.toLowerCase().indexOf("m") === 0)); 

Auch hier verwenden wir zunächst einmal wieder eine Filterung. Bei der per Pfeilfunktion angegebenen Filter-Bedingung verwenden wir eine weitere Higher-Order-Funktion nämlich Array.prototype.every(). Hier müssen wir ebenfalls eine Funktion übergeben, die dieses Mal als Bedingung ausdrückt, was für jeden Wert unseres Subarrays gelten soll (erster Buchstabe ein m).

Every liefert genau dann true zurück, wenn die Bedingung für jeden Wert erfüllt ist. Die Bedingung ist so wesentlich lesbarer, da wir sie im Gegensatz zu dem Beispiel mit den verschachtelten Schleifen nicht invertieren müssen.

Wir können die Lesbarkeit durch Verwendung von charAt anstatt von indexOf meiner Meinung nach noch etwas verbessern:

 let mPersons = data.filter(person =>
  person.every(value => value.toLowerCase().charAt(0) === "m")); 

Das ist aber nur meine persönliche Meinung, da es einfach mehr einem normalen Satz entspricht (indexOf - "Position von m ist Anfang" vs. charAt - "Anfangszeichen ist m").

Wem die Anweisung zu lang ist und gerne noch ein wenig mehr Lesbarkeit in diese bringen möchte, der könnte jetzt noch die innere Pfeilfunktion auslagern und mit einem sprechenden Bezeichner versehen.

 let isValueStartingWithM = value => value.toLowerCase().charAt(0) === "m";
let mPersons = data.filter(person => person.every(isValueStartingWithM)); 

Hierfür legen wir die Funktion einfach nur in einer Variablen ab und übergeben diese als Parameter an every.

Beispiel 4:

Passend zu every gibt es auch noch die Higher-Order-Funktion Array.prototype.some(). Some liefert true zurück, wenn mindestens ein Wert die als Funktion übergebene Bedindung erfüllt.

Nehmen wir beispielsweise einmal an, daß wir alle Personen haben wollen, bei denen ein beliebiger Wert den Buchstaben o enthält.

 let oPersons = [];
for (let i = 0; i < data.length; i++) {
  let check = false;
  for (let j = 0; j < data[i].length; j++) {
    if (data[i][j].toLowerCase().indexOf("o") !== -1) {
      check = true;
    }
  }
  if (check) {
    oPersons.push(data[i]);
  }
} 

Auch hier würde man typischerweise wieder mit verschachtelten Schleifen arbeiten, doch mit some geht dies wesentlich kürzer und lesbarer.

 let isValueWithO = value => value.toLowerCase().indexOf("o") !== -1;
let oPersons = data.filter(person => person.some(isValueWithO)); 

Die zweite Zeile ist nun ungefähr so notiert, wie man sie auch als Satz formulieren würde. Der Datensatz data ist zu filtern und für jede Person in diesem Datensatz soll mindestens ein Wert ein "Wert mit o" sein.

Fazit

Mit Higher-Order-Funktionen kann man vieles wesentlich kürzer ausdrücken, was man früher nur mit (verschachtelten) Schleifen lösen konnte. Zudem steigt die Lesbarkeit erheblich, da man nun sofort sieht, was das Ziel eines Ausdrucks ist (z.B. Mapping oder Filterung).

Mit array_map und array_filter ist ähnliches in PHP übrigens ebenfalls möglich. Some und every gibt es derzeit leider nicht nativ in PHP, man kann diese jedoch relativ einfach als eigene Funktionen oder über eine fertige Funktions-Bibliothek wie beispielsweise phunctional nachrüsten. Letztere habe ich jedoch noch nicht verwendet.

Hallo! Bist du neu hier? Dann abonniere doch den RSS-Feed dieses nicht mehr ganz so stillen Örtchens, um über meine geistigen Ergüsse auf dem Laufenden zu bleiben. Alternativ besteht auch die Möglichkeit, sich von FeedBurner per E-Mail über meine Ausscheidungen benachrichtigen zu lassen.

Neuen Kommentar schreiben

Der Inhalt dieses Feldes wird nicht öffentlich zugänglich angezeigt.
Der Inhalt dieses Feldes wird öffentlich zugänglich angezeigt, aber als rel="nofollow" markiert.
Hinweis

Kommentare beleben den Blog! Ich freue mich über jeden Kommentar. Du kannst hier offen Deine Meinung zum Artikel sagen, aber bitte beachte die Netiquette und vermeide es andere zu beleidigen.

Bitte unterlasst es die Kommentare zu SEO-Zwecken zu missbrauchen. Kommentare mit Links, die nicht zu Blogs führen (oder zu Blogs mit Grauzonen-Themen) und/oder Keywords als Namen verwenden, sind nicht erwünscht!

Möchtest Du mir einen Blog-Artikel schmackhaft machen, dann schreib die URL ohne HTML-Tag in den Kommentarbereich und ich werde diesen bei Gefallen verlinken.