Guten Tag, Freunde!
Ich präsentiere Ihnen eine angepasste Übersetzung des neuen Vorschlags (September 2020) zur Verwendung von Dekoratoren in JavaScript mit einer kleinen Erklärung, was passiert.
Dieser Vorschlag wurde erstmals vor ungefähr 5 Jahren gemacht und hat seitdem einige bedeutende Änderungen erfahren. Es befindet sich derzeit (noch) in der zweiten Phase der Prüfung.
Wenn Sie noch nie von Dekorateuren gehört haben oder Ihr Wissen auffrischen möchten, empfehle ich Ihnen, die folgenden Artikel zu lesen:
- JavaScript-Dekorateure
- Eine minimale Anleitung für ECMAScript-Dekorateure
- JavaScript-Dekorateure von Grund auf neu
Was ist ein Dekorateur? Ein Dekorator ist eine Funktion, die während ihrer Definition für ein Element einer Klasse (Feld oder Methode) oder für die Klasse selbst aufgerufen wird und das Element (oder die Klasse) durch einen neuen Wert (vom Dekorator zurückgegeben) umschließt oder ersetzt.
Ein dekoriertes Klassenfeld wird als Wrapper von einem Getter / Setter behandelt, sodass Sie diesem Feld einen Wert abrufen / zuweisen (ändern) können.
Dekorateure können ein Klassenmitglied auch mit Metadaten versehen. Metadaten sind eine Sammlung einfacher Objekteigenschaften, die von Dekorateuren hinzugefügt wurden. Sie sind als Satz verschachtelter Objekte in der Eigenschaft [Symbol.metadata] verfügbar.
Syntax
Die Decorator-Syntax setzt zusätzlich zum Präfix @ (@decoratorName) Folgendes voraus:
- Dekoratorausdrücke beschränken sich auf die variable Verkettung (es können mehrere Dekoratoren verwendet werden), den Zugriff auf die Eigenschaft mit., Aber nicht mit [] und den Aufruf mit ()
- Es können nicht nur Klassendefinitionen dekoriert werden, sondern auch deren Elemente (Felder und Methoden)
- Klassendekoratoren werden nach dem Export und der Standardeinstellung angegeben
Es gibt keine besonderen Regeln für die Definition von Dekorateuren. Jede Funktion kann als solche verwendet werden.
Details zur Semantik
Der Dekorateur wird in drei Schritten bewertet:
- Der Decorator-Ausdruck (was auch immer auf @ folgt) wird zusammen mit berechneten Eigenschaftsnamen ausgewertet
- Der Dekorator wird (als Funktion) während der Klassendefinition aufgerufen, nachdem die Methoden bewertet wurden, aber bevor der Konstruktor und der Prototyp kombiniert wurden
- Decorator wird nur einmal nach dem Aufruf angewendet (ändert Konstruktor und Prototyp)
1. Computerdekorateure
Dekorateure werden als Ausdrücke zusammen mit berechneten Eigenschaftsnamen ausgewertet. Dies geschieht von links nach rechts und von oben nach unten. Das Ergebnis des Dekorators wird in einer Art lokaler Variable gespeichert, die nach Abschluss der Klassendefinition aufgerufen (verwendet) wird.
2. Dekorateure anrufen
Der Dekorator wird mit zwei Argumenten aufgerufen: dem umschlossenen Element und optional dem Kontextobjekt.
Umhülltes Element: erster Parameter
Das erste Argument, das der Dekorateur umhüllt, ist das, was wir dekorieren (Entschuldigung für die Tautologie):
- Wenn es um eine einfache Methode, Initialisierungsmethode, Getter oder Setter geht: die entsprechende Funktion
- Wenn es um die Klasse geht: die Klasse selbst
- Wenn über Feld: ein Objekt mit zwei Eigenschaften:
- get: Eine Funktion ohne Parameter, die mit einem Empfänger aufgerufen wird. Dies ist ein Objekt, das den darin enthaltenen Wert zurückgibt
- set: Eine Funktion, die einen Parameter (neuen Wert) verwendet, der mit einem Empfänger aufgerufen wird, der das übergebene Objekt ist, und undefiniert zurückgibt
Kontextobjekt: zweiter Parameter
Das Kontextobjekt - das Objekt, das als zweites Argument an den Dekorateur übergeben wird - enthält die folgenden Eigenschaften:
- Art: hat einen der folgenden Werte:
- "Klasse"
- "Methode"
- "Init-Methode"
- "Getter"
- "Setter"
- "Feld"
- Name:
- öffentliches Feld oder Methode: Name - Zeichenfolge oder Zeicheneigenschaftsschlüssel
- privates Feld oder Methode: keine
- Klasse: abwesend
- isStatic:
- statisches Feld oder Methode: true
- Instanzfeld oder Methode: false
- Klasse: abwesend
Das "Ziel" (Konstruktor oder Prototyp) wird nicht an die Feld- oder Methodendekorateure übergeben, da es (das "Ziel") zum Zeitpunkt des Aufrufs des Dekorators noch nicht erstellt wurde.
Rückgabewert
Der Rückgabewert hängt vom Typ des Dekorateurs ab:
- Klasse: neue Klasse
- Methode, Getter oder Setter: neue Funktion
- Feld: Ein Objekt mit drei Eigenschaften:
- bekommen
- einstellen
- initialize: Eine Funktion, die mit demselben Argument wie set aufgerufen wird und den Wert zurückgibt, der zum Initialisieren der Variablen verwendet wurde. Diese Funktion wird aufgerufen, wenn die Einstellung des zugrunde liegenden Speichers vom Feldinitialisierer oder der Methodendefinition abhängt
- init-Methode: Ein Objekt mit zwei Eigenschaften:
- Methode: Eine Funktion, die eine Methode ersetzt
- initialize: Eine Funktion ohne Argumente, deren Rückgabewert ignoriert wird und die mit dem neu erstellten Objekt als Empfänger aufgerufen wird
3. Dekorateure anwenden
Dekorateure werden nach dem Aufruf angewendet. Die Zwischenstufen des Arbeitsalgorithmus des Dekorators können nicht festgelegt werden. Auf die neu erstellte Klasse kann erst zugegriffen werden, wenn alle Dekoratoren der Methoden- und Instanzfelder angewendet wurden.
Klassendekoratoren werden aufgerufen, nachdem Feld- und Methodendekoratoren angewendet wurden.
Schließlich werden statische Felddekoratoren angewendet.
Semantik von Felddekorateuren
Ein Klassenfelddekorateur ist ein Getter / Setter-Paar für ein privates Feld. Daher der Code:
function id(v) { return v }
class C {
@id x = y
}
hat die folgende Semantik:
class C {
// # -
#x = y
get x() { return this.#x }
set x(v) { this.#x = v }
}
Felddekorateure verhalten sich wie private Felder. Der folgende Code löst einen TypeError aus, da wir versuchen, auf "y" zuzugreifen, bevor wir ihn der Instanz hinzufügen:
class C {
@id x = this.y
@id y
}
new C // TypeError
Das Getter / Setter-Paar sind gewöhnliche Methoden für ein Objekt, die wie andere Methoden nicht aufzählbar sind (wenn Sie so wollen, nicht aufzählbar). Die darin enthaltenen privaten Felder werden zusammen mit den Initialisierern wie normale private Felder einzeln hinzugefügt.
Designziele
- Es sollte so einfach sein, die eingebauten Dekorateure zu verwenden, wie es sein sollte, eigene zu schreiben
- Dekorateure sollten nur auf dekorierte Objekte ohne Nebenwirkungen angewendet werden.
Anwendungsfälle
- Speichern von Metadaten in Klassen und Methoden
- Konvertieren eines Felds in einen Accessor
- Umschließen einer Methode oder Klasse (diese Verwendung von Dekoratoren ähnelt dem Objekt-Proxy)
Beispiele von
Beispiele für die Implementierung und Verwendung von Dekorateuren.
@logged
Der @logged-Dekorator druckt Nachrichten über den Beginn und das Ende der Methodenausführung an die Konsole. Es gibt andere beliebte Dekorateure, die Funktionen wie @deprecated einschließen. entprellen, @memoize usw.
Verwenden von:
// .mjs -
import { logged } from './logged.mjs'
class C {
@logged
m(arg) {
this.#x = arg
}
@logged
set #x(value) { }
}
new C().m(1)
// m 1
// set #x 1
// set #x
// m
@logged kann in JavaScript als Dekorateur implementiert werden. Ein Dekorator ist eine Funktion, die mit einem Argument aufgerufen wird, das das zu dekorierende Element enthält. Dieses Element kann eine Methode, ein Getter oder ein Setter sein. Dekorateure können mit einem zweiten Argument aufgerufen werden, dem Kontext, in diesem Fall brauchen wir ihn jedoch nicht.
Der vom Dekorateur zurückgegebene Wert ersetzt das umschlossene Element. Bei Methoden, Gettern und Setzern ist der Rückgabewert die Funktion, die sie ersetzt.
// logged.mjs
export function logged(f) {
//
const name = f.name
function wrapped(...args) {
//
console.log(` ${name} ${args.join(', ')}`)
//
const ret = f.call(this, ...args)
//
console.log(` ${name}`)
//
return ret
}
// Object.defineProperty()
// https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
Object.defineProperty(wrapped, 'name', { value: name, configurable: true })
//
return wrapped
}
Das Transpilationsergebnis des angegebenen Beispiels kann folgendermaßen aussehen:
let x_setter
class C {
m(arg) {
this.#x = arg
}
static #x_setter(value) { }
// - (class static initialization blocks)
// https://github.com/tc39/proposal-class-static-block
static { x_setter = C.#x_setter }
set #x(value) { return x_setter.call(this, value) }
}
C.prototype.m = logged(C.prototype.m, { kind: "method", name: "m", isStatic: false })
x_setter = logged(x_setter, {kind: "setter", isStatic: false})
Beachten Sie, dass Getter und Setter separat dekoriert werden. Accessoren (berechnete Eigenschaften) werden nicht wie in den vorherigen Abschnitten kombiniert.
@defineElement
Mit benutzerdefinierten HTML-Elementen (benutzerdefinierte Elemente, Teil von Webkomponenten) können Sie Ihre eigenen HTML-Elemente erstellen. Die Registrierung der Elemente erfolgt mit customElements.define . So können Sie ein Element mithilfe von Dekoratoren registrieren:
import { defineElement } from './defineElement.js'
@defineElement('my-class')
class MyClass extends HTMLElement { }
Klassen können zusammen mit Methoden und Accessoren dekoriert werden.
// defineElement.mjs
export function defineElement(name, options) {
return klass => {
customElements.define(name, klass, options); return klass
}
}
Der Dekorateur verwendet Argumente, die er selbst verwendet, und wird daher als Funktion implementiert, die eine andere Funktion zurückgibt. Sie können sich das als "Dekorateurfabrik" vorstellen: Nachdem Sie Argumente übergeben haben, erhalten Sie einen anderen Dekorateur.
Dekorateure, die Metadaten hinzufügen
Dekorateure können Klassenmitgliedern Metadaten bereitstellen, indem sie dem an sie übergebenen Kontextobjekt eine Metadateneigenschaft hinzufügen. Alle Objekte, die Metadaten enthalten, werden mit Object.assign verkettet und in die Klasseneigenschaft [Symbol.metadata] eingefügt. Zum Beispiel:
//
@annotate({x: 'y'}) @annotate({v: 'w'}) class C {
//
@annotate({a: 'b'}) method() { }
//
@annotate({c: 'd'}) field
}
C[Symbol.metadata].class.x // 'y'
C[Symbol.metadata].class.v // 'w'
// , , ,
C[Symbol.metadata].prototype.methods.method.a // 'b'
//
C[Symbol.metadata].instance.fields.field.c // 'd'
Bitte beachten Sie, dass das Präsentationsformat des mit Anmerkungen versehenen Objekts ungefähr ist und weiter verfeinert werden kann. Die Hauptaufgabe des Beispiels besteht darin, zu zeigen, dass eine Anmerkung nur ein Objekt ist, für das keine Bibliotheken zum Lesen oder Schreiben von Daten erforderlich sind. Sie wird vom System automatisch erstellt.
Der betreffende Dekorateur kann folgendermaßen implementiert werden:
function annotate(metadata) {
return (_, context) => {
context.metadata = metadata
return _
}
}
Jedes Mal, wenn der Dekorator aufgerufen wird, wird ein neuer Kontext an ihn übergeben, und die Metadateneigenschaft wird, sofern sie nicht undefiniert ist, in [Symbol.metadata] aufgenommen.
Beachten Sie, dass Metadaten, die der Klasse selbst und nicht ihrer Methode hinzugefügt wurden, für in der Klasse deklarierte Dekorateure nicht verfügbar sind. Das Hinzufügen von Metadaten zu einer Klasse erfolgt im Konstruktor, nachdem alle "internen" Dekoratoren aufgerufen wurden, um Datenverlust zu vermeiden.
@tracked
Der @tracked Decorator beobachtet das Klassenfeld und ruft die Rendermethode auf, wenn der Setter aufgerufen wird. Dieses Muster und ähnliche Muster werden häufig von verschiedenen Frameworks verwendet, um das Problem des erneuten Renderns zu lösen.
Die Semantik des Dekorierens von Feldern setzt einen Getter / Setter-Wrapper um einen privaten Datenspeicher voraus. @tracked kann ein Getter / Setter-Paar umschließen, um die Rendering-Logik zu implementieren:
import {tracked} from './tracked.mjs'
class Element {
@tracked counter = 0
increment() { this.counter++ }
render() { console.log(counter) }
}
const e = new Element()
e.increment() // 1
e.increment() // 2
Beim Dekorieren eines Felds ist der "umschlossene" Wert ein Objekt mit zwei Eigenschaften: Funktionen zum Abrufen und Festlegen des internen Speichers. Sie sind so konzipiert, dass sie automatisch an eine Instanz gebunden werden (mithilfe von call ()).
// tracked.mjs
export function tracked({ get, set }) {
return {
get,
set(value) {
if (get.call(this) !== value) {
set.call(this, value)
this.render()
}
}
}
}
Eingeschränkter Zugang zu privaten Feldern und Methoden
Manchmal muss Code außerhalb der Klasse auf private Felder oder Methoden zugreifen. Zum Beispiel, um Interoperabilität zwischen zwei Klassen bereitzustellen oder um Code innerhalb einer Klasse zu testen.
Dekorateure ermöglichen den Zugriff auf private Felder und Methoden. Diese Logik kann in ein Objekt eingekapselt werden, wobei nach Bedarf private Referenzschlüssel bereitgestellt werden.
import { PrivateKey } from './private-key.mjs'
let key = new PrivateKey()
export class Box {
@key.show #contents
}
export function setBox(box, contents) {
return key.set(box, contents)
}
export function getBox(box) {
return key.get(box)
}
Beachten Sie, dass das obige Beispiel eine Art Hack ist, der mit Konstrukten wie dem Verweisen auf private Namen mit private.name oder dem Erweitern des Bereichs von privaten Namen mit private / with einfacher zu implementieren ist . Es zeigt jedoch, wie dieser Vorschlag die vorhandene Funktionalität organisch erweitert.
// private-key.mjs
export class PrivateKey {
#get
#set
show({ get, set }) {
assert(this.#get === undefined && this.#set === undefined)
this.#get = get
this.#set = set
return { get, set }
}
get(obj) {
return this.#get.call(obj)
}
set(obj, value) {
return this.#set.call(obj, value)
}
}
@deprecated
Der @deprecated Decorator druckt eine Warnung an die Konsole über die Verwendung veralteter Felder, Methoden oder Accessoren. Anwendungsbeispiel:
import { deprecated } from './deprecated.mjs'
export class MyClass {
@deprecated field
@deprecated method() { }
otherMethod() { }
}
Damit der Dekorator mit verschiedenen Elementen der Klasse arbeiten kann, informiert das Feld kind des Kontexts den Dekorator über den Typ des als veraltet erkannten syntaktischen Konstrukts. Mit dieser Technik können Sie auch Ausnahmen auslösen, wenn ein Dekorator in einem ungültigen Kontext verwendet wird. Beispiel: Eine innere Klasse kann nicht als veraltet markiert werden, da ihr der Zugriff nicht verweigert werden kann.
function wrapDeprecated(fn) {
let name = fn.name
function method(...args) {
console.warn(` ${name} `)
return fn.call(this, ...args)
}
Object.defineProperty(method, 'name', { value: name, configurable: true })
return method
}
export function deprecated(element, { kind }) {
switch (kind) {
case 'method':
case 'getter':
case 'setter':
return wrapDeprecated(element)
case 'field': {
let { get, set } = element
return { get: wrapDeprecated(get), set: wrapDeprecated(set) }
}
default:
// 'class'
throw new Error(`${kind} @deprecated`)
}
}
Methodendekoratoren, die eine Vorkonfiguration erfordern
Einige Methodendekoratoren verlassen sich darauf, Code auszuführen, wenn die Klasse instanziiert wird. Zum Beispiel:
- Der @ on-Dekorator ('event') für Klassenmethoden erweitert HTMLElement, das diese Methode als Ereignishandler im Konstruktor registriert
- Der @bound-Dekorator entspricht this.method = this.method.bind (this) im Konstruktor
Es gibt verschiedene Möglichkeiten, die genannten Dekorateure zu verwenden.
Option 1: Konstruktoren und Metadaten
Diese Dekoratoren sind eine Kombination aus Metadaten und einem Mixin, das Initialisierungsoperationen enthält, die im Konstruktor verwendet werden.
@on mit einer Berührung
class MyClass extends WithActions(HTMLElement) {
@on('click') clickHandler() {}
}
Der angegebene Dekorateur kann folgendermaßen definiert werden:
// ,
// Symbol
// https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Symbol
const handler = Symbol('handler')
function on(eventName) {
return (method, context) => {
context.metadata = { [handler]: eventName }
return method
}
}
class MetadataLookupCache {
// ,
// WeakMap
// https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
#map = new WeakMap()
#name
constructor(name) { this.#name = name }
get(newTarget) {
let data = this.#map.get(newTarget)
if (data === undefined) {
data = []
let klass = newTarget
while (klass !== null && !(this.#name in klass)) {
for (const [name, { [this.#name]: eventName }] of Object.entries(klass[Symbol.metadata].instance.methods)) {
if (eventName !== undefined) {
data.push({ name, eventName })
}
}
klass = klass.__proto__
}
this.#map.set(newTarget, data)
}
return data
}
}
const handlersMap = new MetadataLookupCache(handler)
function WithActions(superClass) {
return class C extends superClass {
constructor(...args) {
super(...args)
const handlers = handlersMap.get(new.target, C)
for (const { name, eventName } of handlers) {
this.addEventListener(eventName, this[name].bind(this))
}
}
}
}
@gebunden mit einem Mixin
@bound kann folgendermaßen verwendet werden:
class C extends WithBoundMethod(Object) {
#x = 1
@bound method() { return this.#x }
}
const c = new C()
const m = c.method
m() // 1, TypeError
Die Implementierung des Dekorateurs könnte folgendermaßen aussehen:
const boundName = Symbol('boundName')
function bound(method, context) {
context.metadata = { [boundName]: true }
return method
}
const boundMap = new MetadataLookupCache(boundName)
function WithBoundMethods(superClass) {
return class C extends superClass {
constructor(...args) {
super(...args)
const names = boundMap.get(new.target, C)
for (const { name } of names) {
this[name] = this[name].bind(this)
}
}
}
}
Beachten Sie, dass in beiden Beispielen MetadataLookupCache verwendet wird. Beachten Sie auch, dass dieser und der folgende Satz die Verwendung einer Standardbibliothek zum Hinzufügen von Metadaten voraussetzen.
Option 2: Methodendekorateure drin
Dekorateur drin: Bestimmt für Fälle, in denen eine Initialisierungsoperation erforderlich ist, die Superklasse / das Mixin jedoch nicht aufgerufen werden kann. Es ermöglicht das Hinzufügen solcher Operationen, wenn der Konstruktor ausgeführt wird.
@on c init
Verwenden von:
class MyElement extends HTMLElement {
@init: on('click') clickHandler()
}
Dekorateur drin: wird genau wie Methodendekoratoren aufgerufen, gibt jedoch ein Paar {method, initialize} zurück, wobei initialize mit einer neuen Instanz als this-Wert ohne Argumente aufgerufen wird und nichts zurückgibt.
function on(eventName) {
return (method, context) => {
assert(context.kind === 'init-method')
return { method, initialize() { this.addEventListener(eventName, method) } }
}
}
@gebunden mit init
drin: kann auch verwendet werden, um einen Dekorateur zu bauen drin: gebunden:
class C {
#x = 1
@init: bound method() { return this.#x }
}
const c = new C()
const m = c.method
m() // 1, TypeError
Der @bound Dekorator kann folgendermaßen implementiert werden:
function bound(method, { kind, name }) {
assert(kind === 'init-method')
return { method, initialize() { this[name] = this[name].bind(this) } }
}
Weitere Informationen zu den Nutzungsbeschränkungen sowie offene Fragen, die Entwickler vor der Standardisierung von Dekorateuren in JavaScript lösen müssen, finden Sie im Text des Vorschlags unter dem Link am Anfang des Artikels.
Lassen Sie mich diesbezüglich Abschied nehmen. Danke für die Aufmerksamkeit.