Das function-Keyword ist in modernem JavaScript ein Code Smell und man sollte es nicht mehr verwenden. Es einzusetzen ist nicht direkt ein schlimmer Fehler, aber seine Nutzung steht meines Erachtens unter dringendem Rechtfertigungsdruck. function-Functions haben fast keine wünschenswerten Fähigkeiten, dafür allerhand Legacy-Anhängsel, mit denen man sich eigentlich nicht mehr herumschlagen möchte. Stattdessen sollte man in so gut wie jedem Fall zu Arrow Functions oder zur Klassensyntax greifen.

Warum function-Functions mal nützlich waren

Dass JavaScript lange Jahre überhaupt zu gebrauchen war, lag unter anderem daran, dass function-Functions so unglaublich vielseitig sind. In Abwesenheit anderer Features (wie Klassen) übernahmen function-Functions in althergebrachtem JS gleich vier Rollen auf einmal:

  • Als normale Funktion foo() verwendet fungieren function-Functions als normale Funktionen bzw. Prozeduren
  • Mit new aufgerufene function-Functions funktionieren als Constructor-Funktionen
  • Mit new aufgerufene function-Functions dienen, wenn ihre prototype-Eigenschaft entsprechend bestückt sind, als eine Art Klassendeklaration (d.h. als der Ort, in dem Objekt-Methoden gesammelt werden)
  • Als Property eines Objekts aufgerufene function-Functions (z.B. obj.foo()) fungieren als Methode dieses Objekts.

Bemerkenswert, was ein einzelnes Sprachkonstrukt so alles leisten kann! Je nachdem wie ein Funktionsaufruf formuliert wird, kann die Funktion verschiedene Rollen einnehmen. Das Ganze funktioniert (unter anderem) indem jede Funktonsaufrufformulierung den Wert der in jeder Funktion verfügbaren magischen Variable this ändert. Aber genau damit fangen die Probleme von function-Functions an.

Die Nachteile von function-Functions

Der größte Haken an function-Functions ist, dass ihr Verhalten von der Formulierung des Funktionsaufrufs abhängt! Das gleiche Funktonsobjekt kann als foo(), obj.foo() und new foo() aufgerufen werden, obwohl es vermutlich für exakt einen dieser Einsatzzwecke ausgelegt wurde. Das Problem lässt sich einhegen, indem man den Strict Mode verwendet und in seine Funktionen Code einbaut, der die nicht eingeplante Aufrufvarianten entweder unterstützt oder mit Exceptions quittiert. Im besten Fall entsteht dabei unnötiger, fehleranfälliger Boilerplate-Code und im schlimmsten Fall macht sich niemand die Mühe.

// Diese Mühe machen sich die Wenigsten
function MyClass () {
  if (!(this instanceof MyClass)) {
    throw new Error("'new' fehlt");
  }
}

// Diese Mühe macht sich niemand
function myFunc () {
  if (this && this !== window) {
    throw new Error("Keine Klasse oder Methode");
  }
}

Ein weiterer Nachteil: selbst wenn man sich von OOP und Vererbung fernhält, muss man sich als Autor von function-Functions immer noch mit der Existenz von OOP-Features herumschlagen. Auch als normale Funktionen auslegte Funktionen haben, wenn sie function-Functions sind, noch immer die klassischen JS-OOP-Features this und prototype im Gepäck. Gleiches gilt für lästige Legacy-Anhängsel wie arguments, die man im Angesicht moderner Alternativen einfach nicht mehr braucht. In function-Functions sind sie aber stets verfügbar, nur in Arrow Functions nicht.

Zu guter Letzt kommen function-Functions in zwei Varianten daher: Funktionsdeklaration und Funktionsausdruck.

// Funktionsdeklaration
function foo () {}

// Funktionsausdruck
const foo = function () {}

Diese beiden Definitionen einer Funktion namens foo sind fast, aber nicht exakt gleich, da nur Funktionsdeklarationen gehoisted werden. Dadurch können sie aufgerufen werden, bevor sie im Code vorkommen. Ein wirklich notwendiges Feature ist das nicht, aber es ist eine weitere valide (überflüssige) Funktionsvariante mit subtilen Eigenheiten, deren Existenz wertvolle Gehirnkapazität belegt. Aber das muss alles nicht sein!

Alternativen zur function-Function

Statt sich mit den komplizierten function-Functions herumzuschlagen kann man sich mit gezielter Wahl alternativer Sprachmittel das Leben sehr viel leichter machen. So sind beispielsweise für „normale“ Funktionen Arrow Functions das eigentliche Mittel der Wahl. Sie haben kein eigenes this und können daher nicht als Objekt-Methode missbraucht werden. Die prototype-Eigenschaft fehlt und ein Aufruf-Versuch via new wird mit einer Exception quittiert.

const myFunc = () => {
  console.log("this", this);
  console.log("arguments", arguments);
};

myFunc.prototype;
// > undefined

myFunc();
// > "this" window {}
// > ReferenceError: arguments is not defined

new myFunc()
// > TypeError: myFunc is not a constructor

Eine Arrow Function ist eine wahre Funktion, nichts anderes – und die Befreiung von Legacy-Features wie arguments ist inklusive

Wer statt einfacher Funktionen eher Objekte und Methode braucht, ist mit einer Klasse am besten beraten. Nicht nur herrscht in Klassen standardmäßig Strict Mode, auch führt ein Aufruf ohne new zu einer Exception. In den Klassen notierte Methoden profitieren ebenfalls vom Strict Mode und können nur als Objekt-Methoden aufgerufen werden, ansonsten hat ihr this den Wert undefined:

class MyClass {
  foo () {
    console.log(this);
  }
}

const instance = new MyClass();
const foo = instance.foo;

instance.foo();
// > MyClass {}

foo();
// > undefined

MyClass()
// > TypeError: Class constructor MyClass cannot be invoked without 'new'

Klassen haben zwar wie function-Functions eine Deklarations- und eine Ausdruckssyntax, aber da erstere nicht gehoisted wird, ist das wirklich ein rein syntaktisches Detail:

// funktioniert nicht
new FooExpression();
const FooExpression = function () {};

// funktioniert!
new FooDeclaration();
function FooDeclaration () {}

// funktioniert nicht
new BarExpression();
const BarExpression = class {};

// funktioniert auch nicht
new BarDeclaration();
class BarDeclaration {}

Es zeigt sich: Klassen und Methoden sind präzise Werkzeuge um Objekte und ihre Methoden zu formulieren – und nichts anderes!

Verbleibende Use Cases für function-Function

Es gibt nach meinem Kenntnisstand zwei Fälle, in denen function-Functions das Mittel der Wahl sind. Der erste Fall betrifft TypeScript, wo die Syntax das Überladen der Typsignaturen von Funktionsdeklarationen (d.h. function-Functions), nicht aber von Arrow Functions zulässt:

// Überladen ist mit Arrow Functions nicht möglich
function foo <T> (input: T[], selector: (item: T) => 0 | 1): [ T[], T[] ];
function foo <T> (input: T[], selector: (item: T) => 0 | 1 | 2): [ T[], T[], T[] ];
function foo <T> (input: T[], selector: (item: T) => number): T[][] {
  // Implementierung
}

Fall zwei ist das Patchen von Prototypen. Hier braucht es eine Funktion, die mit this umgehen kann, aber außerhalb einer Klasse formuliert werden kann. Das kann nur eine function-Function sein:

SomeClass.prototype.newMethod = function () {
  // Implementierung
};

Letzteres ist schon ziemlich nah an der Grenze zum Hack angesiedelt. Unter Umständen nützlich bzw. nötig, aber ganz sicher kein Alltags-JavaScript.

Fazit

function-Functions sind an sich keine Katastrophe. Da sie aber viele verschiedene Use Cases auf einmal abdecken und diverse Legacy-Features mit sich herumschleppen, während es gleichzeitig pro Use Case eine einfachere, spezifischere Funktionssyntax ohne Legacy-Feature gibt, gibt es kaum noch einen Grund, function-Functions einzusetzen! In so gut wie jedem Fall sind Arrow Functions oder Klassen die bessere Wahl, da sie für ihre spezifischen Use Cases die spezifischeren Werkzeuge sind und sich in der Verwendung als weniger fehleranfällig erweisen. In heutigem JavaScipt steht jede function-Function unter Rechtfertigungsdruck.