Entdecken Sie JavaScript Decorators: Fügen Sie Metadaten hinzu, transformieren Sie Klassen/Methoden und erweitern Sie die Funktionalität Ihres Codes auf saubere, deklarative Weise.
JavaScript Decorators: Metadaten und Transformation
JavaScript Decorators, ein Feature, das von Sprachen wie Python und Java inspiriert ist, bieten eine leistungsstarke und ausdrucksstarke Möglichkeit, Metadaten hinzuzufügen und Klassen, Methoden, Eigenschaften und Parameter zu transformieren. Sie bieten eine saubere, deklarative Syntax zur Erweiterung der Code-Funktionalität und zur Förderung der Trennung von Belangen (Separation of Concerns). Obwohl sie eine relativ neue Ergänzung im JavaScript-Ökosystem sind, gewinnen Decorators an Popularität, insbesondere in Frameworks wie Angular und Bibliotheken, die Metadaten für Dependency Injection und andere fortgeschrittene Funktionen nutzen. Dieser Artikel untersucht die Grundlagen von JavaScript Decorators, ihre Anwendung und ihr Potenzial zur Schaffung von wartbareren und erweiterbareren Codebasen.
Was sind JavaScript Decorators?
Im Kern sind Decorators spezielle Arten von Deklarationen, die an Klassen, Methoden, Accessoren, Eigenschaften oder Parameter angehängt werden können. Sie verwenden die @expression
-Syntax, wobei expression
zu einer Funktion ausgewertet werden muss, die zur Laufzeit mit Informationen über die dekorierte Deklaration aufgerufen wird. Decorators fungieren im Wesentlichen als Funktionen, die das Verhalten des dekorierten Elements modifizieren oder erweitern.
Stellen Sie sich Decorators als eine Möglichkeit vor, bestehenden Code zu umschließen oder zu erweitern, ohne ihn direkt zu verändern. Dieses Prinzip, das im Software-Design als Decorator-Pattern bekannt ist, ermöglicht es Ihnen, einem Objekt dynamisch Funktionalität hinzuzufügen.
Aktivierung von Decorators
Obwohl Decorators Teil des ECMAScript-Standards sind, sind sie in den meisten JavaScript-Umgebungen nicht standardmäßig aktiviert. Um sie zu verwenden, müssen Sie in der Regel Ihre Build-Tools konfigurieren. Hier erfahren Sie, wie Sie Decorators in einigen gängigen Umgebungen aktivieren können:
- TypeScript: Decorators werden in TypeScript nativ unterstützt. Stellen Sie sicher, dass die Compiler-Option
experimentalDecorators
in Ihrertsconfig.json
-Datei auftrue
gesetzt ist:
{
"compilerOptions": {
"target": "esnext",
"experimentalDecorators": true,
"emitDecoratorMetadata": true, // Optional, aber oft nützlich
"module": "commonjs", // Oder ein anderes Modulsystem wie "es6" oder "esnext"
"moduleResolution": "node"
}
}
- Babel: Wenn Sie Babel verwenden, müssen Sie das
@babel/plugin-proposal-decorators
-Plugin installieren und konfigurieren:
npm install --save-dev @babel/plugin-proposal-decorators
Fügen Sie dann das Plugin zu Ihrer Babel-Konfiguration hinzu (z. B. .babelrc
oder babel.config.js
):
{
"plugins": [["@babel/plugin-proposal-decorators", { "version": "2023-05" }]]
}
Die Option version
ist wichtig und sollte mit der Version des Decorator-Proposals übereinstimmen, die Sie anstreben. Konsultieren Sie die Dokumentation des Babel-Plugins für die neueste empfohlene Version.
Arten von Decorators
Es gibt verschiedene Arten von Decorators, die jeweils für bestimmte Elemente konzipiert sind:
- Klassen-Decorators: Werden auf Klassen angewendet.
- Methoden-Decorators: Werden auf Methoden innerhalb einer Klasse angewendet.
- Accessor-Decorators: Werden auf Getter- oder Setter-Accessoren angewendet.
- Eigenschafts-Decorators: Werden auf Eigenschaften einer Klasse angewendet.
- Parameter-Decorators: Werden auf Parameter einer Methode oder eines Konstruktors angewendet.
Klassen-Decorators
Klassen-Decorators werden auf den Konstruktor einer Klasse angewendet und können verwendet werden, um eine Klassendefinition zu beobachten, zu modifizieren oder zu ersetzen. Sie erhalten den Klassenkonstruktor als einziges Argument.
Beispiel:
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
// Der Versuch, der versiegelten Klasse oder ihrem Prototyp Eigenschaften hinzuzufügen, wird fehlschlagen
In diesem Beispiel verhindert der @sealed
-Decorator weitere Änderungen an der Greeter
-Klasse und ihrem Prototyp. Dies kann nützlich sein, um die Unveränderlichkeit sicherzustellen oder versehentliche Änderungen zu verhindern.
Methoden-Decorators
Methoden-Decorators werden auf Methoden innerhalb einer Klasse angewendet. Sie erhalten drei Argumente:
target
: Der Prototyp der Klasse (für Instanzmethoden) oder der Klassenkonstruktor (für statische Methoden).propertyKey
: Der Name der dekorierten Methode.descriptor
: Der Property Descriptor für die Methode.
Beispiel:
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey} with arguments: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`Method ${propertyKey} returned: ${result}`);
return result;
};
return descriptor;
}
class Calculator {
@log
add(x: number, y: number) {
return x + y;
}
}
const calculator = new Calculator();
calculator.add(2, 3); // Ausgabe: Calling add with arguments: [2,3]
// Method add returned: 5
Der @log
-Decorator protokolliert die Argumente und den Rückgabewert der add
-Methode. Dies ist ein einfaches Beispiel dafür, wie Methoden-Decorators für Protokollierung, Profiling oder andere übergreifende Belange (Cross-Cutting Concerns) verwendet werden können.
Accessor-Decorators
Accessor-Decorators ähneln Methoden-Decorators, werden aber auf Getter- oder Setter-Accessoren angewendet. Sie erhalten ebenfalls dieselben drei Argumente: target
, propertyKey
und descriptor
.
Beispiel:
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() {
return this._x;
}
set x(value: number) {
this._x = value;
}
}
const point = new Point(1, 2);
// Object.defineProperty(point, 'x', { configurable: true }); // Würde einen Fehler auslösen, da 'x' nicht konfigurierbar ist
Der @configurable(false)
-Decorator verhindert, dass der x
-Getter neu konfiguriert werden kann, wodurch er nicht konfigurierbar wird.
Eigenschafts-Decorators
Eigenschafts-Decorators werden auf Eigenschaften einer Klasse angewendet. Sie erhalten zwei Argumente:
target
: Der Prototyp der Klasse (für Instanzeigenschaften) oder der Klassenkonstruktor (für statische Eigenschaften).propertyKey
: Der Name der dekorierten Eigenschaft.
Beispiel:
function readonly(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
writable: false,
});
}
class Person {
@readonly
name: string;
constructor(name: string) {
this.name = name;
}
}
const person = new Person("Alice");
// person.name = "Bob"; // Dies würde im Strict Mode einen Fehler verursachen, da 'name' schreibgeschützt ist
Der @readonly
-Decorator macht die name
-Eigenschaft schreibgeschützt und verhindert, dass sie nach der Initialisierung geändert wird.
Parameter-Decorators
Parameter-Decorators werden auf Parameter einer Methode oder eines Konstruktors angewendet. Sie erhalten drei Argumente:
target
: Der Prototyp der Klasse (für Instanzmethoden) oder der Klassenkonstruktor (für statische Methoden oder Konstruktoren).propertyKey
: Der Name der Methode oder des Konstruktors.parameterIndex
: Der Index des Parameters in der Parameterliste.
Parameter-Decorators werden oft mit Reflection verwendet, um Metadaten über die Parameter einer Funktion zu speichern. Diese Metadaten können dann zur Laufzeit für Dependency Injection oder andere Zwecke verwendet werden. Damit dies korrekt funktioniert, müssen Sie die Compiler-Option emitDecoratorMetadata
in Ihrer tsconfig.json
-Datei aktivieren.
Beispiel (mit reflect-metadata
):
import 'reflect-metadata';
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let existingRequiredParameters: number[] = Reflect.getOwnMetadata("required", target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata("required", existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor) {
let method = descriptor.value!;
descriptor.value = function (...args: any[]) {
let requiredParameters: number[] = Reflect.getOwnMetadata("required", target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (args[parameterIndex] === null || args[parameterIndex] === undefined) {
throw new Error(`Missing required argument at index ${parameterIndex}`);
}
}
}
return method.apply(this, args);
};
}
class User {
name: string;
age: number;
constructor(@required name: string, public surname: string, @required age: number) {
this.name = name;
this.age = age;
}
@validate
greet(prefix: string, @required salutation: string): string {
return `${prefix} ${salutation} ${this.name}`;
}
}
// Verwendung
try {
const user1 = new User("John", "Doe", 30);
console.log(user1.greet("Mr.", "Hello"));
const user2 = new User(undefined as any, "Doe", null as any);
} catch (error) {
console.error(error.message);
}
try {
const user = new User("John", "Doe", 30);
console.log(user.greet("Mr.", undefined as any));
} catch (error) {
console.error(error.message);
}
In diesem Beispiel markiert der @required
-Decorator Parameter als erforderlich. Der @validate
-Decorator verwendet dann Reflection (über reflect-metadata
), um zu prüfen, ob die erforderlichen Parameter vorhanden sind, bevor die Methode aufgerufen wird. Dieses Beispiel zeigt die grundlegende Verwendung, und es wird empfohlen, eine robuste Parameter-Validierung in einem Produktionsszenario zu erstellen.
Um reflect-metadata
zu installieren:
npm install reflect-metadata --save
Verwendung von Decorators für Metadaten
Eine der Hauptanwendungen von Decorators ist das Anhängen von Metadaten an Klassen und deren Mitglieder. Diese Metadaten können zur Laufzeit für verschiedene Zwecke verwendet werden, wie z.B. Dependency Injection, Serialisierung und Validierung. Die reflect-metadata
-Bibliothek bietet eine Standardmethode zum Speichern und Abrufen von Metadaten.
Beispiel:
import 'reflect-metadata';
const TYPE_KEY = "design:type";
const PARAMTYPES_KEY = "design:paramtypes";
const RETURNTYPE_KEY = "design:returntype";
function Type(type: any) {
return Reflect.metadata(TYPE_KEY, type);
}
function LogType(target: any, propertyKey: string) {
const t = Reflect.getMetadata(TYPE_KEY, target, propertyKey);
console.log(`${target.constructor.name}.${propertyKey} type: ${t.name}`);
}
class Demo {
@LogType
public name: string;
constructor(name: string){
this.name = name;
}
}
Decorator Factories
Decorator Factories sind Funktionen, die einen Decorator zurückgeben. Sie ermöglichen es Ihnen, Argumente an den Decorator zu übergeben, was ihn flexibler und wiederverwendbarer macht.
Beispiel:
function deprecated(deprecationReason: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.warn(`Method ${propertyKey} is deprecated: ${deprecationReason}`);
return originalMethod.apply(this, args);
};
return descriptor;
};
}
class LegacyComponent {
@deprecated("Use the newMethod instead.")
oldMethod() {
console.log("Old method called");
}
newMethod() {
console.log("New method called");
}
}
const component = new LegacyComponent();
component.oldMethod(); // Ausgabe: Method oldMethod is deprecated: Use the newMethod instead.
// Old method called
Die @deprecated
-Decorator-Factory nimmt eine Veraltungsnachricht als Argument entgegen und protokolliert eine Warnung, wenn die dekorierte Methode aufgerufen wird. Dies ermöglicht es Ihnen, Methoden als veraltet zu kennzeichnen und Entwicklern eine Anleitung zur Migration auf neuere Alternativen zu geben.
Anwendungsfälle aus der Praxis
Decorators haben ein breites Anwendungsspektrum in der modernen JavaScript-Entwicklung:
- Dependency Injection: Frameworks wie Angular setzen stark auf Decorators für die Dependency Injection.
- Routing: In Webanwendungen können Decorators verwendet werden, um Routen für Controller und Methoden zu definieren.
- Validierung: Decorators können verwendet werden, um Eingabedaten zu validieren und sicherzustellen, dass sie bestimmte Kriterien erfüllen.
- Autorisierung: Decorators können verwendet werden, um Sicherheitsrichtlinien durchzusetzen und den Zugriff auf bestimmte Methoden oder Ressourcen zu beschränken.
- Protokollierung und Profiling: Wie in den obigen Beispielen gezeigt, können Decorators für die Protokollierung und das Profiling der Codeausführung verwendet werden.
- State Management: Decorators können sich in State-Management-Bibliotheken integrieren, um Komponenten automatisch zu aktualisieren, wenn sich der Zustand ändert.
Vorteile der Verwendung von Decorators
- Verbesserte Lesbarkeit des Codes: Decorators bieten eine deklarative Syntax zum Hinzufügen von Funktionalität, was den Code leichter verständlich und wartbar macht.
- Trennung von Belangen: Decorators ermöglichen es Ihnen, übergreifende Belange (z.B. Protokollierung, Validierung, Autorisierung) von der Kerngeschäftslogik zu trennen.
- Wiederverwendbarkeit: Decorators können über mehrere Klassen und Methoden hinweg wiederverwendet werden, was die Codeduplizierung reduziert.
- Erweiterbarkeit: Decorators machen es einfach, die Funktionalität von bestehendem Code zu erweitern, ohne ihn direkt zu ändern.
Herausforderungen und Überlegungen
- Lernkurve: Decorators sind ein relativ neues Feature, und es kann einige Zeit dauern, bis man lernt, wie man sie effektiv einsetzt.
- Kompatibilität: Stellen Sie sicher, dass Ihre Zielumgebung Decorators unterstützt und dass Sie Ihre Build-Tools korrekt konfiguriert haben.
- Debugging: Das Debuggen von Code, der Decorators verwendet, kann schwieriger sein als das Debuggen von regulärem Code, insbesondere wenn die Decorators komplex sind.
- Übermäßiger Gebrauch: Vermeiden Sie den übermäßigen Gebrauch von Decorators, da dies Ihren Code schwerer verständlich und wartbar machen kann. Setzen Sie sie strategisch für bestimmte Zwecke ein.
- Laufzeit-Overhead: Decorators können einen gewissen Laufzeit-Overhead verursachen, insbesondere wenn sie komplexe Operationen durchführen. Berücksichtigen Sie die Leistungsauswirkungen bei der Verwendung von Decorators in leistungskritischen Anwendungen.
Fazit
JavaScript Decorators sind ein leistungsstarkes Werkzeug zur Erweiterung der Code-Funktionalität und zur Förderung der Trennung von Belangen. Durch die Bereitstellung einer sauberen, deklarativen Syntax zum Hinzufügen von Metadaten und zur Transformation von Klassen, Methoden, Eigenschaften und Parametern können Decorators Ihnen helfen, wartbarere, wiederverwendbarere und erweiterbarere Codebasen zu erstellen. Obwohl sie eine Lernkurve und einige potenzielle Herausforderungen mit sich bringen, können die Vorteile der Verwendung von Decorators im richtigen Kontext erheblich sein. Da sich das JavaScript-Ökosystem weiterentwickelt, werden Decorators wahrscheinlich ein immer wichtigerer Bestandteil der modernen JavaScript-Entwicklung werden.
Erwägen Sie zu untersuchen, wie Decorators Ihren bestehenden Code vereinfachen oder es Ihnen ermöglichen können, ausdrucksstärkere und wartbarere Anwendungen zu schreiben. Mit sorgfältiger Planung und einem soliden Verständnis ihrer Fähigkeiten können Sie Decorators nutzen, um robustere und skalierbarere JavaScript-Lösungen zu erstellen.