Hinterfragt: Woran erkennt man einen guten JavaScript-Entwickler?

the next big thing  –  3 Kommentare

Einen Entwickler adäquat beurteilen zu können, ist für die Teambildung hilfreich. Im Themenbereich JavaScript sind hierfür zwei Aspekte wichtig: Einerseits technische Kenntnisse der Sprache an sich, andererseits ein grundlegendes Verständnis für die in JavaScript gängigen Konzepte. Woran also erkennt man einen guten JavaScript-Entwickler?

Die Fragestellung hat der schwedische Entwickler Mattias Petter Johansson in seinem Blogeintrag "How do you judge a Javascript programmer by only 5 questions?" aufgegriffen, in dem er die fünf folgenden Fragen vorschlägt:

  • Was ist der Unterschied zwischen call und apply?
  • Wie funktioniert die map-Funktion?
  • Wie funktioniert die bind-Funktion?
  • Wie funktionieren Closures?
  • Wie lassen sich Leistungsprobleme in JavaScript lösen?

Die ersten vier Fragen zielen auf technische Aspekte der Sprache JavaScript und auf allgemeine Konzepte gleichermaßen ab, die fünfte Frage stellt er bewusst im Kontext der persönlichen Erfahrung: Welchen Problemen ist man bereits begegnet und wie ließen sie sich lösen?

1. Was ist der Unterschied zwischen call und apply?

In JavaScript gibt es verschiedene Möglichkeiten, eine Funktion aufzurufen. Sie unterscheiden sich letztlich darin, an was das Schlüsselwort this gebunden wird. Ruft man eine Funktion "as is" auf, verweist this auf den globalen Kontext, was im Browser dem window-Objekt entspricht:

'use strict';

let hi = function () {
console.log(this.text);
};

hi();

Da im Beispiel der Strict-Mode verwendet wird, verhindert Node.js den Zugriff auf this und gibt eine entsprechende Fehlermeldung aus:

TypeError: Cannot read property 'text' of undefined

Alternativ lässt sich eine Funktion an einem Objekt aufrufen. Das Schlüsselwort this verweist dann auf das Objekt. Im Unterschied zu Sprachen wie C# oder Java ist in JavaScript also nicht von Belang, wo die Funktion definiert wurde, sondern nur, wie sie aufgerufen wird:

'use strict';

let hello = {
text: 'Hello world!',
world: function () {
console.log(this.text);
}
};

hello.world();

Das Beispiel ist ebenfalls einfach, da die Funktion direkt am Objekt zur Verfügung steht, an dem sie aufgerufen werden soll. Doch was, wenn stattdessen die weiter vorne definierte Funktion hi verwendet werden soll? Wie lässt sich eine Beziehung zwischen ihr und dem Objekt hello herstellen?

Der Aufruf von

hello.hi();

schlägt aus naheliegenden Gründen mit der Fehlermeldung

TypeError: hello.hi is not a function

fehl. An dieser Stelle kommen die Funktionen call und apply ins Spiel. Sie sind in JavaScript an jeder Funktion verfügbar, da Funktionen wie Objekte eigene Eigenschaften besitzen können. Sie lassen sich verwenden, um this künstlich auf ein anderes Objekt zeigen zu lassen und einen Funktionsaufruf umzubiegen.

Auf dem Weg ist es also möglich, die Funktion hi am Objekt hello aufzurufen, ohne dass das Objekt die Funktion enthalten oder gar kennen muss:

hi.call(hello);

Die apply-Funktion funktioniert auf exakt die gleiche Art. Als Frage bleibt also, warum es sowohl call als auch apply gibt? Die Antwort offenbart sich, sobald Parameter übergeben werden müssen. Angenommen, die Funktion hi erwartet zwei Parameter, die den auszugebenden Text umgeben. Dann sieht ihre Definition wie folgt aus:

let hi = function (prefix, suffix) {
console.log(prefix + this.text + suffix);
};

Beim Aufruf mittels call müssen die Parameter dann mit angegeben werden. Dazu sind sie als durch Kommata separierte Werte aufzuzählen:

hi.call(hello, "You say:", "Yo!");
// => "You say: Hello world! Yo!"

Die apply-Funktion funktioniert nun nicht mehr identisch, sondern nur noch ähnlich: Anstatt jeden Parameter einzeln zu übergeben, erwartet die Funktion ein Array mit allen Parametern:

hi.apply(hello, [ "You say:", "Yo!" ]);
// => "You say: Hello world! Yo!"

Das Ergebnis ist in beiden Fällen das gleiche, lediglich die Art des Aufrufs unterscheidet sich. Während call besonders dann praktisch ist, wenn eine Funktion von Hand aufzurufen ist, eignet sich apply dann, wenn die zu übergebenden Werte bereits als Array vorliegen, beispielsweise im Rahmen von arguments.

2. Wie funktioniert die map-Funktion?

Die map-Funktion ist eine Funktion höherer Ordnung und steht in JavaScript für Arrays zur Verfügung. Sie ist allerdings nicht spezifisch für JavaScript, sondern einer der essenziellen Grundbausteine der funktionalen Programmierung. Daher findet man äquivalente Funktionen auch in zahlreichen anderen Sprachen.

Die grundlegende Idee ist, jeden Wert eines Arrays mit einer Funktion zu verarbeiten und die Ergebnisse wiederum in einem Array zu speichern. Beispielsweise könnte man aus einer Liste von Zahlen die Liste der zugehörigen Quadratzahlen berechnen wollen. Die Abbildung der einen auf die andere Liste leistet die map-Funktion:

'use strict';

let numbers = [ 1, 2, 3, 4, 5 ];

let squares = numbers.map(function (n) {
return n * n;
});

console.log(squares);
// => [ 1, 4, 9, 16, 25 ]

Im Gegensatz zu einer Schleife wird auf dem Weg nicht beschrieben, auf welche Art das Array zu durchlaufen ist: Es obliegt der map-Funktion, ein geeignetes Vorgehen auszuwählen. Das kann potenziell sogar bedeuten, dass die Funktion das Array parallelisiert verarbeitet, um die Leistung zu steigern.

Obwohl die map-Funktion bereits seit Jahrzehnten in Sprachen wie Lisp enthalten ist, hat sie erst in den vergangenen Jahren wieder an Popularität gewonnen, nicht zuletzt aufgrund des Map-/Reduce-Algorithmus.

3. Wie funktioniert die bind-Funktion?

Die bind-Funktion führt im Grunde das Thema aus der ersten Frage fort: Bei ihr geht es darum, eine Funktion dauerhaft an ein bestimmtes Objekt zu binden, auch wenn die Funktion nicht an diesem Objekt aufgerufen wird. Dazu ist bind an der Funktion aufzurufen und das zu bindende Objekt als erster Parameter zu übergeben:

'use strict';

let hi = function (prefix, suffix) {
console.log(prefix + this.text + suffix);
};

let hello = {
text: 'Hello world!'
};

let boundHi = hi.bind(hello);

boundHi('You say:', 'Yo!');
// You say: Hello world! Yo!

Wie sich zeigt, hat bind also den gleichen Effekt wie call. Es lassen sich sogar Parameter auf dem gleichen Weg festlegen, indem man sie bind übergibt. Besonders praktisch ist bind immer dann, wenn eine Funktion mehrfach aufgerufen werden muss, this aber stets auf das gleiche Objekt verweisen soll. Ein typisches Beispiel hierfür sind ereignisbehandelnde Funktionen.

4. Wie funktionieren Closures?

Auf die gleiche Art, wie die dritte Frage Bezug auf die erste nimmt, gehört diese Frage mit der zweiten zusammen: Funktionen höherer Ordnung sind nämlich nicht nur Funktionen, die Funktionen als Parameter entgegennehmen (wie map), sondern auch Funktionen, die Funktionen zurückgeben.

Ein einfaches Beispiel ist eine Funktion, die Funktionen zum Addieren zurückgibt, wobei jeweils einer der beiden Summanden bereits festgeschrieben wurde:

'use strict';

let getAdder = function (first) {
return function (second) {
return first + second;
};
};

let add5 = getAdder(5);

let sum = add5(18);
console.log(sum);
// => 23

Bemerkenswert an der Funktion, die von getAdder zurückgegeben wird, ist, dass sie Zugriff auf den Parameter first hat, obwohl der Stack der Funktion getAdder längst abgeräumt wurde, wenn add5 aufgerufen wird. Der Schlüssel zur Lösung ist, dass es sich bei der Funktion add5 um eine sogenannte Closure handelt: Eine Funktion, die über ihrem Erstellungskontext abgeschlossen ist.

Mit anderen Worten: add5 "erinnert" sich an den Zeitpunkt ihrer Definition und kann daher auf die Werte zugreifen, die zu dem Zeitpunkt als Parameter gegeben waren. Closures werden im Deutschen auch als Funktionsabschlüsse bezeichnet.

Closures sind ein ausgesprochen wichtiges Hilfsmittel in der funktionalen Programmierung, ermöglichen sie die Definition verschachtelter Funktionen, die dynamisch erzeugt werden. Auch die in der vorigen Frage besprochene bind-Funktion ist letztlich nichts anderes als eine Closure, die intern auf apply zurückgreift:

'use strict';

let customBind = function (fn, obj) {
return function () {
fn.apply(obj, arguments);
};
};

Das Beispiel zeigt, wie mächtig funktionale Programmierung in Verbindung mit Closures in JavaScript ist und dass außerdem vieles in JavaScript kein Hexenwerk ist, sondern lediglich eine geschickte Kombination von wenigen elementaren Grundbausteinen.

5. Wie lassen sich Leistungsprobleme in JavaScript lösen?

Für die fünfte und letzte Frage gibt es natürlich zahlreiche mögliche Antworten, von denen nicht eine "die richtige" ist. Ein wichtiger Aspekt, gerade im Zusammenhang mit Closures, ist der Speicherbedarf von Anwendungen: Da sie sich an den Zeitpunkt ihrer Definition erinnern, sind zumindest die relevanten Werte des Stacks weiter zu merken.

Das macht Closures unter Umständen teuer, und es gilt darauf zu achten, keine hängenden Referenzen zu hinterlassen. Das Thema geht allerdings über den Rahmen des Blogeintrags hinaus.

tl;dr: Die vorgestellten Fragen beantworten zu können, zeigt ein wesentliches Verständnis für grundlegende Konstrukte von JavaScript und der funktionalen Programmierung im Allgemeinen.