
JavaScript hat mich vor allem immer überrascht, weil es wahrscheinlich wie keine andere weit verbreitete Sprache beide Paradigmen gleichzeitig unterstützt: normale und abnormale Programmierung. Und wenn fast alles über angemessene Best Practices und Vorlagen gelesen wurde, bleibt die wunderbare Welt, wie Sie keinen Code schreiben sollten, aber können, nur leicht angelehnt.
In diesem Artikel analysieren wir eine weitere erfundene Aufgabe, die den unentschuldbaren Missbrauch einer normalen Lösung erfordert.
Vorherige Aufgabe:
Implementieren Sie eine Dekorationsfunktion, die die Anzahl der Aufrufe der übergebenen Funktion zählt und die Möglichkeit bietet, diese Nummer bei Bedarf abzurufen. Die Lösung verwendet keine geschweiften Klammern und globalen Variablen.
Der Anrufzähler ist nur eine Ausrede, denn es gibt console.count () . Unter dem Strich sammelt unsere Funktion beim Aufrufen einer umschlossenen Funktion einige Daten und bietet eine bestimmte Schnittstelle für den Zugriff darauf. Dies kann die Aufbewahrung aller Ergebnisse des Aufrufs, das Sammeln von Protokollen und eine Art Memoisierung sein. Nur ein Gegenmittel - primitiv und für alle verständlich.
Alle Komplexität ist in einer abnormalen Einschränkung. Sie können keine geschweiften Klammern verwenden, was bedeutet, dass Sie die alltäglichen Praktiken und die gewöhnliche Syntax überdenken müssen.
Gewohnheitsmäßige Lösung
Zuerst müssen Sie einen Ausgangspunkt auswählen. Wenn die Sprache oder ihre Erweiterung nicht die erforderliche Dekorationsfunktion bietet, implementieren wir normalerweise einen eigenen Container: eine umschlossene Funktion, akkumulierte Daten und eine Zugriffsschnittstelle auf diese. Dies ist oft eine Klasse:
class CountFunction { constructor(f) { this.calls = 0; this.f = f; } invoke() { this.calls += 1; return this.f(...arguments); } } const csum = new CountFunction((x, y) => x + y); csum.invoke(3, 7);
Dies passt nicht sofort zu uns, da:
- In JavaScript können Sie eine private Eigenschaft nicht auf diese Weise implementieren: Wir können sowohl die Aufrufe der Instanz (die wir benötigen) lesen als auch den Wert von außen schreiben (was wir NICHT benötigen). Natürlich können wir Closure im Konstruktor verwenden , aber was bedeutet dann die Klasse? Und ich hätte immer noch Angst, frische Privatfelder ohne Babel 7 zu nutzen .
- Die Sprache unterstützt ein funktionales Paradigma, und das Erstellen einer Instanz durch neues scheint hier nicht die beste Lösung zu sein. Es ist schöner, eine Funktion zu schreiben, die eine andere Funktion zurückgibt. Ja!
- Schließlich erlaubt uns die Syntax von ClassDeclaration und MethodDefinition nicht, alle geschweiften Klammern loszuwerden.
Aber wir haben ein wunderbares Modulmuster , das Datenschutz durch Schließen implementiert:
function count(f) { let calls = 0; return { invoke: function() { calls += 1; return f(...arguments); }, getCalls: function() { return calls; } }; } const csum = count((x, y) => x + y); csum.invoke(3, 7);
Damit können Sie bereits arbeiten.
Unterhaltsame Entscheidung
Warum werden hier Zahnspangen verwendet? Dies sind 4 verschiedene Fälle:
- Definieren des Hauptteils einer Zählfunktion ( FunctionDeclaration )
- Initialisierung des zurückgegebenen Objekts
- Die Definition des aufgerufenen Funktionskörpers ( FunctionExpression ) mit zwei Ausdrücken
- Definieren des Hauptteils einer Funktion getCalls ( FunctionExpression ) mit einem einzelnen Ausdruck
Beginnen wir mit dem zweiten Absatz. Tatsächlich müssen wir kein neues Objekt zurückgeben, während der Aufruf der endgültigen Funktion durch Aufrufen erschwert wird. Wir können die Tatsache ausnutzen, dass eine Funktion in JavaScript ein Objekt ist, was bedeutet, dass sie ihre eigenen Felder und Methoden enthalten kann. Lassen Sie uns unsere return df- Funktion erstellen und die getCalls- Methode hinzufügen, die über den Abschluss wie zuvor auf Aufrufe zugreifen kann:
function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = function() { return calls; } return df; }
Es ist angenehmer, damit zu arbeiten:
const csum = count((x, y) => x + y); csum(3, 7);
Der vierte Punkt ist klar: Wir ersetzen nur FunctionExpression durch ArrowFunction . Das Fehlen von geschweiften Klammern liefert uns eine kurze Aufzeichnung der Pfeilfunktion im Fall eines einzelnen Ausdrucks in seinem Körper:
function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = () => calls; return df; }
Mit dem dritten ist alles komplizierter. Denken Sie daran, dass wir als erstes FunctionExpression durch aufgerufene Funktionen durch FunctionDeclaration df ersetzt haben . Um dies in ArrowFunction umzuschreiben, müssen zwei Probleme gelöst werden: den Zugriff auf die Argumente nicht zu verlieren (jetzt ist es ein Pseudo-Array von Argumenten ) und den Funktionskörper zweier Ausdrücke zu vermeiden.
Das erste Problem hilft uns, mit den explizit für den Funktionsparameter args mit dem Spread-Operator angegebenen Problemen umzugehen . Um zwei Ausdrücke zu einem zu kombinieren, können Sie das logische UND verwenden . Im Gegensatz zum klassischen logischen Konjunktionsoperator, der Boolean zurückgibt, berechnet er Operanden von links nach rechts bis zum ersten "false" und gibt sie zurück. Wenn alle "true" sind, dann der letzte Wert. Das allererste Inkrement des Zählers ergibt 1, was bedeutet, dass dieser Unterausdruck immer auf true gesetzt wird. Die Reduzierbarkeit des Ergebnisses des Funktionsaufrufs im zweiten Unterausdruck auf die „Wahrheit“ interessiert uns nicht: Auf jeden Fall bleibt der Rechner dabei stehen. Jetzt können wir die ArrowFunction verwenden :
function count(f) { let calls = 0; let df = (...args) => (calls += 1) && f(...args); df.getCalls = () => calls; return df; }
Sie können einen Datensatz mit dem Präfix-Inkrement ein wenig dekorieren:
function count(f) { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; }
Die Lösung für den ersten und schwierigsten Punkt beginnt mit dem Ersetzen von FunctionDeclaration durch ArrowFunction . Aber wir haben den Körper immer noch in geschweiften Klammern:
const count = f => { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; };
Wenn wir geschweifte Klammern entfernen möchten, die den Hauptteil der Funktion umrahmen, müssen wir vermeiden, Variablen durch let zu deklarieren und zu initialisieren. Und wir haben zwei ganze Variablen: Aufrufe und df .
Lassen Sie uns zuerst den Zähler behandeln. Wir können eine lokale Variable erstellen, indem wir sie in der Liste der Funktionsparameter definieren und den Anfangswert übertragen, indem wir sie mit IIFE (Sofort aufgerufener Funktionsausdruck) aufrufen:
const count = f => (calls => { let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; })(0);
Es bleibt, die drei Ausdrücke zu einem zu verketten. Da wir alle drei Ausdrücke haben, die Funktionen darstellen, die immer auf wahr reduziert werden können, können wir auch das logische UND verwenden :
const count = f => (calls => (df = (...args) => ++calls && f(...args)) && (df.getCalls = () => calls) && df)(0);
Es gibt jedoch noch eine andere Option zum Verketten von Ausdrücken: Verwenden des Kommaoperators . Dies ist vorzuziehen, da keine unnötigen logischen Transformationen behandelt werden und weniger Klammern erforderlich sind. Die Operanden werden von links nach rechts ausgewertet, und das Ergebnis ist der Wert des letzteren:
const count = f => (calls => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0);
Ich glaube, ich habe es geschafft, dich zu täuschen? Wir haben die Deklaration der Variablen df mutig losgeworden und nur die Zuweisung unserer Pfeilfunktion belassen. In diesem Fall wird diese Variable global deklariert, was nicht akzeptabel ist! Für df wiederholen wir die Initialisierung der lokalen Variablen in den Parametern unserer IIFE-Funktion, übergeben jedoch keinen Anfangswert:
const count = f => ((calls, df) => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0);
Damit ist das Ziel erreicht.
Variationen über ein Thema
Interessanterweise konnten wir vermeiden, lokale Variablen, mehrere Ausdrücke in Funktionsblöcken und ein Objektliteral zu erstellen und zu initialisieren. Gleichzeitig wurde die ursprüngliche Lösung sauber gehalten: das Fehlen globaler Variablen, der Schutz der Privatsphäre und der Zugriff auf die Argumente der Funktion, die verpackt wird.
Im Allgemeinen können Sie jede Implementierung übernehmen und versuchen, etwas Ähnliches zu tun. Zum Beispiel ist die Polyfüllung für die Bindungsfunktion in dieser Hinsicht ziemlich einfach:
const bind = (f, ctx, ...a) => (...args) => f.apply(ctx, a.concat(args));
Wenn das Argument f jedoch keine Funktion ist, sollten wir auf gute Weise eine Ausnahme auslösen. Und die Wurfausnahme kann nicht im Kontext des Ausdrucks ausgelöst werden. Sie können auf Wurfausdrücke warten (Stufe 2) und es erneut versuchen. Oder hat jemand schon Gedanken?
Oder betrachten Sie eine Klasse, die die Koordinaten eines Punktes beschreibt:
class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `(${this.x}, ${this.y})`; } }
Was durch eine Funktion dargestellt werden kann:
const point = (x, y) => (p => (px = x, py = y, p.toString = () => ['(', x, ', ', y, ')'].join(''), p))(new Object);
Nur hier haben wir die Prototypvererbung verloren: toString ist eine Eigenschaft des Point- Prototypobjekts, kein separat erstelltes Objekt. Kann dies vermieden werden, wenn Sie sich anstrengen?
In den Ergebnissen der Transformationen erhalten wir eine ungesunde Mischung aus funktionaler Programmierung mit imperativen Hacks und einigen Merkmalen der Sprache selbst. Wenn Sie darüber nachdenken, kann sich dies als interessanter (aber nicht praktischer) Verschleierer des Quellcodes herausstellen. Sie können Ihre eigene Version der Aufgabe "Bracketing Obfuscator" erstellen und Kollegen und Freunde von JavaScript'ern in ihrer Freizeit von nützlicher Arbeit unterhalten.
Fazit
Die Frage ist, für wen ist es nützlich und warum wird es benötigt? Dies ist für Anfänger völlig schädlich, da es eine falsche Vorstellung von der übermäßigen Komplexität und Abweichung der Sprache bildet. Aber es kann für Praktiker nützlich sein, da Sie die Merkmale der Sprache von der anderen Seite betrachten können: Der Anruf ist nicht zu vermeiden, und der Anruf ist zu versuchen, in Zukunft zu vermeiden.