JavaScript-Dekoratoren: Was sie sind und wann man sie verwenden sollte
On Januar 26, 2022 by adminMit der Einführung von ES2015+ und da die Transpilierung alltäglich geworden ist, werden viele von Ihnen auf neuere Sprachfunktionen gestoßen sein, entweder in echtem Code oder in Tutorials. Eines dieser Features, über das man sich oft den Kopf zerbricht, wenn man es zum ersten Mal sieht, sind JavaScript-Dekoratoren.
Dekoratoren sind dank ihrer Verwendung in Angular 2+ populär geworden. In Angular sind Dekoratoren dank TypeScript verfügbar, aber in JavaScript sind sie derzeit ein Vorschlag der Stufe 2, was bedeutet, dass sie Teil eines zukünftigen Updates der Sprache sein sollten. Werfen wir einen Blick darauf, was Dekoratoren sind und wie sie verwendet werden können, um Ihren Code sauberer und verständlicher zu machen.
Was ist ein Dekorator?
In seiner einfachsten Form ist ein Dekorator einfach eine Möglichkeit, ein Stück Code mit einem anderen zu umhüllen – es buchstäblich zu „dekorieren“. Dies ist ein Konzept, das Sie vielleicht schon als funktionale Komposition oder Funktionen höherer Ordnung kennen.
Dies ist bereits in Standard-JavaScript für viele Anwendungsfälle möglich, indem man einfach eine Funktion aufruft, um eine andere zu umhüllen:
function doSomething(name) { console.log('Hello, ' + name);}function loggingDecorator(wrapped) { return function() { console.log('Starting'); const result = wrapped.apply(this, arguments); console.log('Finished'); return result; }}const wrapped = loggingDecorator(doSomething);
Dieses Beispiel erzeugt eine neue Funktion – in der Variablen wrapped
– die genau so aufgerufen werden kann wie die Funktion doSomething
und genau dasselbe tut. Der Unterschied besteht darin, dass vor und nach dem Aufruf der gewrappten Funktion eine Protokollierung erfolgt:
doSomething('Graham');// Hello, Grahamwrapped('Graham');// Starting// Hello, Graham// Finished
Wie man JavaScript-Dekoratoren verwendet
Dekoratoren verwenden eine spezielle Syntax in JavaScript, bei der ihnen ein @
-Symbol vorangestellt und unmittelbar vor dem zu dekorierenden Code platziert wird.
Hinweis: Zum Zeitpunkt der Erstellung dieses Artikels befinden sich die Dekoratoren in der „Stage 2 Draft“-Form, was bedeutet, dass sie größtenteils fertiggestellt sind, aber immer noch Änderungen unterliegen.
Es ist möglich, so viele Dekoratoren für ein und dasselbe Stück Code zu verwenden, wie man möchte, und sie werden in der Reihenfolge angewendet, in der man sie deklariert.
Zum Beispiel:
@log()@immutable()class Example { @time('demo') doSomething() { // }}
Dies definiert eine Klasse und wendet drei Dekoratoren an – zwei auf die Klasse selbst, und einen auf eine Eigenschaft der Klasse:
-
@log
könnte alle Zugriffe auf die Klasse protokollieren -
@immutable
könnte die Klasse unveränderlich machen – vielleicht ruft sieObject.freeze
bei neuen Instanzen auf -
@time
wird aufzeichnen, wie lange eine Methode zur Ausführung braucht und dies mit einem eindeutigen Tag protokollieren.
Zurzeit erfordert die Verwendung von Dekoratoren Transpiler-Unterstützung, da kein aktueller Browser oder Node-Release sie unterstützt. Wenn man Babel verwendet, wird dies einfach durch die Verwendung des transform-decorators-legacy Plugins ermöglicht.
Anmerkung: Der Gebrauch des Wortes „legacy“ in diesem Plugin ist, weil es die Art und Weise unterstützt, wie Babel 5 Dekoratoren handhabt, die sich sehr wohl von der endgültigen Form unterscheiden kann, wenn sie standardisiert sind.
Warum Dekoratoren verwenden?
Während funktionale Komposition in JavaScript bereits möglich ist, ist es wesentlich schwieriger – oder sogar unmöglich – die gleichen Techniken auf andere Teile des Codes anzuwenden (z.B. Klassen und Klasseneigenschaften).
Der Decorator-Vorschlag fügt Unterstützung für Klassen- und Eigenschaftsdekoratoren hinzu, die verwendet werden können, um diese Probleme zu lösen, und zukünftige JavaScript-Versionen werden wahrscheinlich Dekorator-Unterstützung für andere problematische Bereiche des Codes hinzufügen.
Dekoratoren erlauben auch eine sauberere Syntax für die Anwendung dieser Wrapper um Ihren Code, was zu etwas führt, das weniger von der eigentlichen Intention dessen, was Sie schreiben, ablenkt.
Unterschiedliche Arten von Dekoratoren
Zurzeit sind die einzigen Arten von Dekoratoren, die unterstützt werden, auf Klassen und Mitglieder von Klassen. Dazu gehören Eigenschaften, Methoden, Getter und Setter.
Dekoratoren sind eigentlich nichts anderes als Funktionen, die eine andere Funktion zurückgeben, und die mit den entsprechenden Details des zu dekorierenden Elements aufgerufen werden. Diese Dekorator-Funktionen werden einmal ausgewertet, wenn das Programm zum ersten Mal läuft, und der dekorierte Code wird durch den Rückgabewert ersetzt.
Klassenmitglied-Dekoratoren
Eigenschaftsdekoratoren werden auf ein einzelnes Mitglied in einer Klasse angewandt – ob es sich um Eigenschaften, Methoden, Getter oder Setter handelt. Diese Dekoratorfunktion wird mit drei Parametern aufgerufen:
-
target
: die Klasse, in der sich das Mitglied befindet. -
name
: der Name des Mitglieds in der Klasse. -
descriptor
: der Mitgliederdeskriptor. Dies ist im Wesentlichen das Objekt, das an Object.defineProperty übergeben worden wäre.
Das klassische Beispiel, das hier verwendet wird, ist @readonly
. Dies wird so einfach implementiert wie:
function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}
Wortwörtlich eine Aktualisierung des Eigenschaftsdeskriptors, um das Kennzeichen „beschreibbar“ auf false zu setzen.
Dies wird dann auf eine Klasseneigenschaft wie folgt angewendet:
class Example { a() {} @readonly b() {}}const e = new Example();e.a = 1;e.b = 2;// TypeError: Cannot assign to read only property 'b' of object '#<Example>'
Aber wir können noch mehr tun. Wir können die dekorierte Funktion tatsächlich durch ein anderes Verhalten ersetzen. Lassen Sie uns zum Beispiel alle Eingaben und Ausgaben protokollieren:
function log(target, name, descriptor) { const original = descriptor.value; if (typeof original === 'function') { descriptor.value = function(...args) { console.log(`Arguments: ${args}`); try { const result = original.apply(this, args); console.log(`Result: ${result}`); return result; } catch (e) { console.log(`Error: ${e}`); throw e; } } } return descriptor;}
So wird die gesamte Methode durch eine neue ersetzt, die die Argumente protokolliert, die ursprüngliche Methode aufruft und dann die Ausgabe protokolliert.
Beachten Sie, dass wir hier den Spread-Operator verwendet haben, um automatisch ein Array aus allen bereitgestellten Argumenten zu erstellen, was die modernere Alternative zum alten arguments
-Wert ist.
Wir können dies wie folgt in Gebrauch sehen:
class Example { @log sum(a, b) { return a + b; }}const e = new Example();e.sum(1, 2);// Arguments: 1,2// Result: 3
Sie werden feststellen, dass wir eine etwas seltsame Syntax verwenden mussten, um die dekorierte Methode auszuführen. Das könnte einen ganzen Artikel füllen, aber kurz gesagt, die apply
Funktion erlaubt es Ihnen, die Funktion aufzurufen, indem Sie den this
Wert und die Argumente angeben, mit denen sie aufgerufen werden soll.
Wenn wir noch einen Schritt weiter gehen, können wir dafür sorgen, dass unser Dekorator einige Argumente erhält. Schreiben wir zum Beispiel unseren log
-Dekorator wie folgt um:
function log(name) { return function decorator(t, n, descriptor) { const original = descriptor.value; if (typeof original === 'function') { descriptor.value = function(...args) { console.log(`Arguments for ${name}: ${args}`); try { const result = original.apply(this, args); console.log(`Result from ${name}: ${result}`); return result; } catch (e) { console.log(`Error from ${name}: ${e}`); throw e; } } } return descriptor; };}
Das wird jetzt etwas komplexer, aber wenn wir es aufschlüsseln, haben wir folgendes:
- Eine Funktion,
log
, die einen einzigen Parameter benötigt:name
. - Diese Funktion gibt dann eine Funktion zurück, die selbst ein Dekorator ist.
Dieser ist identisch mit dem früheren log
-Dekorator, außer dass er den name
-Parameter von der äußeren Funktion verwendet.
Dies wird dann wie folgt verwendet:
class Example { @log('some tag') sum(a, b) { return a + b; }}const e = new Example();e.sum(1, 2);// Arguments for some tag: 1,2// Result from some tag: 3
Sofort können wir sehen, dass dies uns erlaubt, zwischen verschiedenen Protokollzeilen zu unterscheiden, indem wir ein Tag verwenden, das wir selbst geliefert haben.
Das funktioniert, weil der log('some tag')
Funktionsaufruf von der JavaScript-Laufzeit sofort ausgewertet wird und die Antwort darauf als Dekorator für die sum
Methode verwendet wird.
Klassendekoratoren
Klassendekoratoren werden auf die gesamte Klassendefinition in einem Zug angewendet. Die Dekoratorfunktion wird mit einem einzigen Parameter aufgerufen, nämlich der Konstruktorfunktion, die dekoriert werden soll.
Beachten Sie, dass dies auf die Konstruktorfunktion angewendet wird und nicht auf jede Instanz der Klasse, die erstellt wird. Das bedeutet, dass man, wenn man die Instanzen manipulieren will, dies selbst tun muss, indem man eine verpackte Version des Konstruktors zurückgibt.
Im Allgemeinen sind diese weniger nützlich als Klassenmitglied-Dekoratoren, weil alles, was man hier tun kann, man mit einem einfachen Funktionsaufruf auf genau die gleiche Weise tun kann. Alles, was man damit macht, muss am Ende eine neue Konstruktorfunktion zurückgeben, um den Klassenkonstruktor zu ersetzen.
Zurück zu unserem Protokollierungsbeispiel, lassen Sie uns eines schreiben, das die Konstruktorparameter protokolliert:
function log(Class) { return (...args) => { console.log(args); return new Class(...args); };}
Hier akzeptieren wir eine Klasse als Argument und geben eine neue Funktion zurück, die als Konstruktor fungiert. Diese protokolliert einfach die Argumente und gibt eine neue Instanz der Klasse zurück, die mit diesen Argumenten konstruiert wurde.
Beispiel:
@logclass Example { constructor(name, age) { }}const e = new Example('Graham', 34);// console.log(e);// Example {}
Wir können sehen, dass die Konstruktion unserer Beispielklasse die angegebenen Argumente protokolliert und dass der konstruierte Wert tatsächlich eine Instanz von Example
ist. Genau das, was wir wollten.
Die Übergabe von Parametern an Klassendekoratoren funktioniert genauso wie bei Klassenmitgliedern:
function log(name) { return function decorator(Class) { return (...args) => { console.log(`Arguments for ${name}: args`); return new Class(...args); }; }}@log('Demo')class Example { constructor(name, age) {}}const e = new Example('Graham', 34);// Arguments for Demo: argsconsole.log(e);// Example {}
Beispiele aus der realen Welt
Kerndekoratoren
Es gibt eine fantastische Bibliothek namens Core Decorators, die einige sehr nützliche allgemeine Dekoratoren bereitstellt, die man sofort verwenden kann. Diese ermöglichen im Allgemeinen eine sehr nützliche allgemeine Funktionalität (z.B. Timing von Methodenaufrufen, Verfallswarnungen, Sicherstellung, dass ein Wert schreibgeschützt ist), aber unter Verwendung der viel saubereren Decorator-Syntax.
React
Die React-Bibliothek macht sehr guten Gebrauch von dem Konzept der Higher-Order Components. Dies sind einfach React-Komponenten, die als Funktion geschrieben sind und eine andere Komponente umschließen.
Kaufen Sie unseren Premium-Kurs: React The ES6 Way
Sie sind ein idealer Kandidat für die Verwendung als Dekorator, da man nur sehr wenig ändern muss, um dies zu tun. Zum Beispiel hat die react-redux-Bibliothek eine Funktion, connect
, die verwendet wird, um eine React-Komponente mit einem Redux-Speicher zu verbinden.
Im Allgemeinen würde diese Funktion wie folgt verwendet werden:
class MyReactComponent extends React.Component {}export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
Aufgrund der Funktionsweise der Decorator-Syntax kann diese Funktion jedoch durch den folgenden Code ersetzt werden, um genau die gleiche Funktionalität zu erreichen:
@connect(mapStateToProps, mapDispatchToProps)export default class MyReactComponent extends React.Component {}
MobX
Die MobX-Bibliothek macht ausgiebig Gebrauch von Decoratoren, die es ermöglichen, Felder einfach als Observable oder Computed zu markieren und Klassen als Observer zu markieren.
Zusammenfassung
Dekoratoren für Klassenmitglieder bieten eine sehr gute Möglichkeit, Code innerhalb einer Klasse zu verpacken, ähnlich wie man es bereits bei freistehenden Funktionen tun kann. Dies bietet eine gute Möglichkeit, einfachen Hilfscode zu schreiben, der an vielen Stellen auf eine sehr saubere und leicht verständliche Art und Weise angewendet werden kann.
Die einzige Grenze für die Verwendung einer solchen Einrichtung ist Ihre Vorstellungskraft!
Schreibe einen Kommentar