Meta-Programmierung: Objekte auf einer neuen Ebene kontrollieren
JavaScript Proxy erlaubt es, einen Stellvertreter vor ein Objekt zu schalten, der jeden Lese-, Schreib-, Lösch- und Funktionsaufruf abfängt. Reflect ist das symmetrische Gegenstück: Es ruft die Standardverhalten der Sprache als erstklassige Funktionen auf. Zusammen bilden sie die Grundlage für reaktive Datensysteme, Validierungsschichten, Logging-Proxies und virtuelle Objekte in modernem JavaScript.
Inhaltsverzeichnis
- 1. Was ist Meta-Programmierung mit Proxy?
- 2. Die 13 Traps: vollständige Übersicht der abfangbaren Operationen
- 3. get und set Traps: Validierung und Transformation
- 4. Reflect: Standardverhalten als erstklassige Funktion
- 5. apply und construct Traps: Funktionen und Klassen abfangen
- 6. Reaktive Daten: wie Vue.js 3 und MobX Proxy nutzen
- 7. Revocable Proxies: Zugriff kontrolliert widerrufen
- 8. Proxy-Invarianten: Was Traps nicht verletzen dürfen
- 9. Proxy-Traps im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was ist Meta-Programmierung mit Proxy?
Meta-Programmierung bedeutet, dass Code Aussagen über sich selbst oder andere Code-Strukturen machen und diese verändern kann. In JavaScript ist Proxy der primäre Mechanismus für Meta-Programmierung auf Objekt-Ebene: Man schiebt einen Stellvertreter vor ein Ziel-Objekt, der jeden Zugriff auf das Objekt abfangen kann – lesend, schreibend, löschend, aufzählend, aufrufend. Der Code, der das Objekt verwendet, merkt nichts davon, weil der Proxy nach außen wie das Ziel-Objekt aussieht. Innen kann der Proxy-Handler beliebige Logik ausführen: validieren, transformieren, protokollieren, cachen oder den Zugriff ganz verweigern.
Der Unterschied zu normalen Getter/Setter-Eigenschaften (Object.defineProperty) ist tiefgreifend: Proxy fängt nicht nur den Zugriff auf einzelne Eigenschaften ab, sondern alle fundamentalen Operationen auf dem Objekt – inklusive der Prüfung, ob eine Eigenschaft existiert (in-Operator), dem Löschen von Eigenschaften (delete), dem Aufzählen aller Eigenschaften (for...in), dem Aufruf als Funktion und dem Erstellen via new. Das macht Proxy zur mächtigsten Erweiterungsmöglichkeit für Objekte in JavaScript – mächtiger als Mixins, Decorators oder Prototype-Manipulation.
2. Die 13 Traps: vollständige Übersicht der abfangbaren Operationen
Ein Proxy-Handler kann bis zu 13 verschiedene "Traps" implementieren, die jeweils eine fundamentale Objekt-Operation abfangen. Die wichtigsten im Alltag sind get (Eigenschaft lesen), set (Eigenschaft schreiben), has (in-Operator), deleteProperty (delete-Operator) und apply (Funktionsaufruf). Weniger bekannte, aber ebenso mächtige Traps sind construct (new-Operator), ownKeys (Object.keys() und for...in), getOwnPropertyDescriptor, defineProperty, getPrototypeOf, setPrototypeOf, isExtensible und preventExtensions.
Jede Trap, die nicht im Handler-Objekt definiert ist, fällt automatisch auf das Standardverhalten zurück – als ob die Operation direkt auf dem Ziel-Objekt ausgeführt würde. Das bedeutet, man muss nur die Traps implementieren, die man tatsächlich braucht. Ein Handler mit nur einer get-Trap interceptiert ausschließlich Lesezugriffe; alle anderen Operationen laufen unverändert durch. Diese selektive Abfangbarkeit macht Proxy chirurgisch präzise im Vergleich zu globalem Monkey-Patching.
// Proxy with get and set traps — validation and logging layer
const createValidatedObject = (target, schema) =>
new Proxy(target, {
// Intercept all property reads
get(target, prop, receiver) {
console.log(`[GET] ${String(prop)}`);
return Reflect.get(target, prop, receiver); // default behavior
},
// Intercept all property writes — validate against schema
set(target, prop, value, receiver) {
if (prop in schema) {
const validator = schema[prop];
if (!validator(value)) {
throw new TypeError(
`Invalid value for "${String(prop)}": ${JSON.stringify(value)}`
);
}
}
console.log(`[SET] ${String(prop)} = ${JSON.stringify(value)}`);
return Reflect.set(target, prop, value, receiver); // default behavior
},
// Intercept delete — prevent deletion of required fields
deleteProperty(target, prop) {
if (prop in schema) {
throw new Error(`Cannot delete required property "${String(prop)}"`);
}
return Reflect.deleteProperty(target, prop);
},
});
const user = createValidatedObject({}, {
name: (v) => typeof v === "string" && v.length > 0,
age: (v) => Number.isInteger(v) && v >= 0 && v <= 150,
email: (v) => typeof v === "string" && v.includes("@"),
});
user.name = "Alice"; // OK
user.age = 30; // OK
user.age = -5; // TypeError: Invalid value for "age"
delete user.name; // Error: Cannot delete required property "name"
3. get und set Traps: Validierung und Transformation
Die get-Trap ist die am häufigsten genutzte und vielseitigste Trap. Sie empfängt drei Parameter: target (das Ziel-Objekt), prop (der Eigenschaftsname als String oder Symbol) und receiver (das Proxy-Objekt selbst, relevant für korrekte this-Bindung bei Gettern). Sie kann einen beliebigen Wert zurückgeben – den echten Wert, einen transformierten Wert, einen Default-Wert für fehlende Eigenschaften, oder das Ergebnis einer Datenbankabfrage für ein "virtuelles Objekt".
Die set-Trap empfängt vier Parameter: target, prop, value und receiver. Sie muss einen booleschen Wert zurückgeben: true für Erfolg, false für Ablehnung (was im strikten Modus einen TypeError auslöst). Ein häufiger Fehler: Vergessen, true zurückzugeben, wenn die Zuweisung erfolgreich war. Das führt im strikten Modus zu einem TypeError nach jeder Zuweisung, auch wenn der Wert korrekt im Ziel-Objekt gesetzt wurde. Reflect.set() gibt korrekt true oder false zurück und ist deshalb die bevorzugte Implementierung des Default-Verhaltens innerhalb einer set-Trap.
4. Reflect: Standardverhalten als erstklassige Funktion
Reflect ist ein eingebautes Objekt, das alle fundamentalen Objekt-Operationen als Methoden bereitstellt, die sonst nur als Sprachsyntax zugänglich sind. Reflect.get(target, prop, receiver) tut dasselbe wie target[prop], aber als aufrufbare Funktion. Reflect.set(target, prop, value, receiver) tut dasselbe wie target[prop] = value. Das macht Reflect zum idealen Companion für Proxy: In jeder Trap kann man nach eigener Logik auf Reflect zurückfallen, um das Standardverhalten korrekt und invariantenkonform auszuführen.
Warum Reflect statt direktem Zugriff auf target? Der entscheidende Unterschied liegt im receiver-Parameter. Wenn ein Proxy für eine Klassen-Instanz erstellt wird und die Klasse Getter-Methoden hat, die this verwenden, muss der Getter mit dem Proxy als this aufgerufen werden – sonst verweist this auf das Ziel-Objekt statt auf den Proxy. Reflect.get(target, prop, receiver) übergibt den receiver korrekt an Getter, während target[prop] den receiver ignoriert und so für klassen-basierte Objekte mit Gettern falsch verhalten kann.
// Reflect.get vs direct access — why receiver matters for class getters
class Temperature {
#celsius;
constructor(c) { this.#celsius = c; }
get fahrenheit() { return this.#celsius * 9/5 + 32; } // uses 'this'
}
const temp = new Temperature(100);
const proxy = new Proxy(temp, {
get(target, prop, receiver) {
console.log(`Accessing: ${String(prop)}`);
// CORRECT: passes receiver — 'this' in getter refers to proxy
return Reflect.get(target, prop, receiver);
// WRONG: return target[prop]; — 'this' in getter is the target, not proxy
},
});
console.log(proxy.fahrenheit); // 212 — correct with Reflect.get
// Reflect as standalone: all fundamental operations as callable functions
const obj = { x: 1, y: 2 };
Reflect.set(obj, "z", 3); // same as obj.z = 3, returns true/false
Reflect.has(obj, "x"); // same as "x" in obj → true
Reflect.deleteProperty(obj, "y"); // same as delete obj.y → true
Reflect.ownKeys(obj); // ["x", "z"] — all own keys incl. Symbols
Reflect.defineProperty(obj, "w", { value: 4, enumerable: true, configurable: true });
5. apply und construct Traps: Funktionen und Klassen abfangen
Die apply-Trap fängt Funktionsaufrufe ab – sowohl direkte Aufrufe als auch .call() und .apply(). Sie empfängt target (die Funktion), thisArg (den this-Kontext) und argumentsList (die Argumente als Array). Ein Proxy mit einer apply-Trap kann Funktionsaufrufe protokollieren, Argumente validieren, Rückgabewerte transformieren oder die Funktion ganz ersetzen. Das ist das Fundament für Aspect-Oriented Programming in JavaScript – ohne Bibliothek und ohne Decorators.
Die construct-Trap fängt new Klasse()-Aufrufe ab. Sie empfängt target (die Klasse), argumentsList und newTarget. Mit der construct-Trap kann man Factory-Muster implementieren, die Erstellung von Instanzen kontrollieren, Singletons erzwingen oder Dependency-Injection auf Klassen-Ebene realisieren. Da sowohl apply als auch construct das Ziel-Objekt eine Funktion sein müssen, kann ein einziger Proxy keine normalen Objekte mit diesen Traps ausstatten – der Versuch führt zu einem TypeError.
6. Reaktive Daten: wie Vue.js 3 und MobX Proxy nutzen
Vue.js 3 hat sein Reaktivitätssystem komplett auf Proxy umgestellt – weg vom Object.defineProperty-basierten Ansatz aus Vue 2. Das Kernkonzept: Ein reaktives Objekt ist ein Proxy, dessen get-Trap beim Lesen einer Eigenschaft den aktuellen Watcher (Komponente oder Computed-Property) als Abhängigkeit registriert. Die set-Trap benachrichtigt alle registrierten Abhängigkeiten, wenn sich der Wert ändert. Dieses System ist tiefer als das Vue-2-System, weil es auch dynamisch hinzugefügte Eigenschaften und Array-Index-Zugriffe ohne spezielle Vue.set()-Syntax erfasst.
MobX 6 verwendet denselben Ansatz für seinen observable()-Wrapper. Jedes observable()-Objekt ist ein Proxy, der Lese-Zugriffe in Reaktionen aufzeichnet und Schreib-Zugriffe als Action propagiert. Das macht MobX transparent und transparent-reaktiv: Normaler JavaScript-Code – Arrays, Objekte, Klassen – kann ohne Annotationen reaktiv gemacht werden, weil der Proxy alle Zugriffe unsichtbar instrumentiert. Dieser Ansatz ist leistungsfähiger und ergonomischer als explizite Signal-Bibliotheken, aber erfordert ein tiefes Verständnis der Proxy-Semantik, um Randfall-Verhalten korrekt zu erklären.
// Minimal reactive system using Proxy (simplified Vue 3 / MobX pattern)
let activeEffect = null;
const deps = new WeakMap(); // target → Map(prop → Set(effects))
function getDep(target, prop) {
if (!deps.has(target)) deps.set(target, new Map());
const map = deps.get(target);
if (!map.has(prop)) map.set(prop, new Set());
return map.get(prop);
}
function reactive(target) {
return new Proxy(target, {
get(target, prop, receiver) {
// Track: register current effect as dependency
if (activeEffect) getDep(target, prop).add(activeEffect);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
const result = Reflect.set(target, prop, value, receiver);
// Trigger: notify all effects depending on this prop
getDep(target, prop).forEach(effect => effect());
return result;
},
});
}
function effect(fn) {
activeEffect = fn;
fn(); // run once to collect dependencies
activeEffect = null;
}
// Usage: reactive state with automatic dependency tracking
const state = reactive({ count: 0, name: "Alice" });
effect(() => console.log(`Count is now: ${state.count}`)); // logs immediately
state.count++; // triggers effect: "Count is now: 1"
state.count++; // triggers effect: "Count is now: 2"
state.name = "Bob"; // does NOT trigger the count effect
7. Revocable Proxies: Zugriff kontrolliert widerrufen
Neben dem normalen new Proxy(target, handler) bietet JavaScript auch Proxy.revocable(target, handler). Diese Variante gibt ein Objekt mit zwei Eigenschaften zurück: proxy (der normale Proxy) und revoke (eine Funktion, die den Proxy dauerhaft deaktiviert). Nach dem Aufruf von revoke() wirft jeder Zugriff auf den Proxy – lesend oder schreibend – einen TypeError. Das Ziel-Objekt bleibt unberührt.
Revocable Proxies sind besonders nützlich in Sicherheitskontexten: Ein API-Schlüssel oder ein Datenbankhandle kann als revocable Proxy weitergegeben werden, und nach dem Ende der erlaubten Zugriffszeit wird revoke() aufgerufen. Alle bestehenden Referenzen auf den Proxy werden wertlos, ohne dass man alle Referenz-Halter kennen und benachrichtigen müsste. Das ist das Principle of Least Authority (POLA) angewendet auf JavaScript-Objekte – ein fundamentales Sicherheitsprinzip für sichere Objekt-Capabilities.
| Trap | Abgefangene Operation | Typischer Anwendungsfall |
|---|---|---|
| get | obj.prop, obj["prop"] |
Default-Werte, Lazy Loading, Caching, Logging |
| set | obj.prop = val |
Validierung, Reaktivität, Immutability |
| has | "prop" in obj |
Virtuelle Eigenschaften simulieren |
| apply | fn(), fn.call() |
Memoization, Logging, AOP-Aspekte |
| construct | new Class() |
Factory-Muster, Singleton, Dependency Injection |
8. Proxy-Invarianten: Was Traps nicht verletzen dürfen
Die Spezifikation definiert für jede Trap eine Reihe von Invarianten – Regeln, die die Trap nicht verletzen darf, egal welche Logik sie implementiert. Wenn eine Trap eine Invariante verletzt, wirft JavaScript automatisch einen TypeError, bevor der abfangende Code zurückkehren kann. Das schützt das JavaScript-Typsystem vor Inkonsistenzen, die sonst zu undefiniertem Verhalten führen würden.
Das wichtigste Beispiel: Die get-Trap darf für eine unveränderliche, konfigurationslose Eigenschaft des Ziel-Objekts nicht einen anderen Wert zurückgeben als den tatsächlichen Wert. Wenn Object.defineProperty(target, "x", { value: 42, writable: false, configurable: false }) aufgerufen wurde, muss die get-Trap immer 42 für "x" zurückgeben. Diese Invariante verhindert, dass ein Proxy Lügen über den Inhalt eines nicht änderbaren Objekts erzählt. Wer Proxy-Traps implementiert, sollte diese Invarianten kennen – Verletzungen führen zu kryptischen TypeErrors, die schwer zu debuggen sind.
// Revocable Proxy — time-limited access to sensitive resources
function createTimedAccess(target, durationMs) {
const { proxy, revoke } = Proxy.revocable(target, {
get(target, prop, receiver) {
console.log(`[Access] ${String(prop)} at ${Date.now()}`);
return Reflect.get(target, prop, receiver);
},
});
// Revoke access after the allowed duration
setTimeout(() => {
revoke();
console.log("[Access revoked] All further access will throw TypeError");
}, durationMs);
return proxy;
}
const sensitiveData = { apiKey: "secret-key-123", userId: 42 };
const timedProxy = createTimedAccess(sensitiveData, 5000);
console.log(timedProxy.apiKey); // OK: "[Access] apiKey at …" + "secret-key-123"
// After 5 seconds:
// timedProxy.apiKey; // TypeError: Cannot perform 'get' on a revoked proxy
// apply trap: function call logging and memoization
function memoize(fn) {
const cache = new Map();
return new Proxy(fn, {
apply(target, thisArg, args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log(`[cache hit] ${key}`);
return cache.get(key);
}
const result = Reflect.apply(target, thisArg, args);
cache.set(key, result);
return result;
},
});
}
const expensiveCalc = memoize((n) => n ** 2);
expensiveCalc(10); // computed: 100
expensiveCalc(10); // [cache hit] [10] → 100
Mironsoft
Fortgeschrittene JavaScript-Architektur und reaktive Systemdesigns
Reaktive Datenarchitektur oder Validierungsschicht aufbauen?
Wir implementieren Proxy-basierte Validierungsschichten, reaktive State-Systeme und AOP-Logging-Infrastruktur für skalierbare JavaScript-Architekturen.
Architektur-Beratung
Proxy-Einsatz für Validierung, Reaktivität und AOP in bestehenden Projekten
Implementation
Reaktive Datensysteme, Memoization-Layer und Logging-Proxies für Node.js und Browser
Review
Code-Review für bestehende Proxy-Implementierungen auf Invarianten und Performance
10. Zusammenfassung
JavaScript Proxy und Reflect bilden zusammen das mächtigste Meta-Programmierungs-Werkzeug der Sprache. Proxy erlaubt es, alle fundamentalen Objekt-Operationen abzufangen: Lesen, Schreiben, Löschen, Aufzählen, Funktionsaufruf und Konstruktion. Reflect stellt das Standardverhalten jeder dieser Operationen als erstklassige Funktion bereit und ist der korrekte Weg, das Standard-Handling innerhalb einer Trap weiterzureichen – insbesondere wegen der korrekten receiver-Behandlung für Klassen-Getter.
Die praktischen Anwendungsfälle sind vielfältig: Validierungsschichten ohne manuelle Checks, reaktive Datensysteme wie in Vue 3 und MobX, revocable Proxies für zeitlich begrenzte Ressourcen-Zugänge, Memoization ohne Wrapper-Funktionen, AOP-Logging und virtuelle Objekte. Wer die 13 Traps und die Proxy-Invarianten kennt, kann nahezu jedes cross-cutting Concern in JavaScript elegant mit einem Proxy lösen – transparent für den konsumierenden Code und ohne Änderungen an der Business-Logik.
JavaScript Proxy und Reflect — Das Wichtigste auf einen Blick
Proxy
Stellvertreter vor Objekte. 13 Traps für alle fundamentalen Operationen. Selektiv: nur implementierte Traps werden abgefangen.
Reflect
Standardverhalten als Funktion. In Traps immer Reflect.get/set/… mit receiver verwenden – korrekte this-Bindung für Klassen-Getter.
Anwendungsfälle
Validierung, Reaktivität (Vue 3, MobX), Memoization, AOP-Logging, Revocable-Zugänge, virtuelle Objekte, Dependency-Injection.
Invarianten
Traps dürfen die Proxy-Invarianten nicht verletzen. Beispiel: get darf für non-configurable/non-writable Eigenschaften nicht lügen.