JS
() =>
JavaScript · Proxy · Reflect · Meta-Programmierung
JavaScript Proxy und Reflect
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.

14 Min. Lesezeit Proxy · Reflect · Traps · Revocable · Reaktive Daten Alle modernen Browser · Node.js 6+

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.

11. FAQ: JavaScript Proxy und Reflect

1Was ist ein JavaScript Proxy?
Stellvertreter-Objekt vor einem Ziel. Fängt alle fundamentalen Operationen über Handler-Traps ab. Konsumierender Code merkt keinen Unterschied zum Original.
2Was ist Reflect?
Fundamentale Objekt-Operationen als aufrufbare Methoden. Reflect.get/set/has etc. mit korrekter receiver-Behandlung für Klassen-Getter.
3Warum Reflect.get() statt target[prop]?
Korrekte receiver-Weitergabe für Klassen-Getter. Ohne receiver verweist this im Getter auf das Ziel-Objekt statt auf den Proxy.
4Was ist ein Revocable Proxy?
Proxy.revocable() gibt { proxy, revoke } zurück. Nach revoke() wirft jeder Zugriff TypeError. Ideal für zeitlich begrenzte Ressourcen-Zugänge (POLA).
5Was sind Proxy-Invarianten?
Regeln, die Traps nicht verletzen dürfen. Verletzungen lösen automatischen TypeError aus. Wichtigste: get darf non-configurable/non-writable Eigenschaften nicht lügen.
6Wie nutzt Vue.js 3 Proxy?
reactive() erstellt Proxy. get-Trap registriert Watcher als Abhängigkeit. set-Trap benachrichtigt Abhängigkeiten. Automatisches Dependency Tracking ohne Vue.set().
7Wie viele Traps gibt es?
13: get, set, has, deleteProperty, apply, construct, ownKeys, getOwnPropertyDescriptor, defineProperty, getPrototypeOf, setPrototypeOf, isExtensible, preventExtensions.
8Proxy mit WeakRef kombinieren?
Ja. WeakRef hält Proxy schwach. Wird GC'ed wenn keine starken Referenzen existieren. Ziel-Objekt bleibt unberührt bei anderen Referenzen darauf.
9Ist Proxy langsamer?
2–10× langsamer als direkter Zugriff durch Handler-Dispatch. Für die meisten Fälle vernachlässigbar. In hot-paths mit Millionen Zugriffen/s vermeiden.
10Kann Proxy Primitive wrappen?
Nein. Ziel muss ein Objekt oder eine Funktion sein. Strings, Numbers, Symbols etc. können nicht als Proxy-Ziel verwendet werden.