Deep JavaScript
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

18 Metaprogrammierung mit Proxies



18.1 Überblick

Proxies ermöglichen es uns, Operationen auf Objekten abzufangen und anzupassen (z. B. das Lesen von Eigenschaften). Sie sind ein Metaprogrammierungs-Feature.

Im folgenden Beispiel

Wir fangen nur eine Operation ab – get (Lesen von Eigenschaften)

const logged = [];

const target = {size: 0};
const handler = {
  get(target, propKey, receiver) {
    logged.push('GET ' + propKey);
    return 123;
  }
};
const proxy = new Proxy(target, handler);

Wenn wir die Eigenschaft proxy.size lesen, fängt der Handler diese Operation ab.

assert.equal(
  proxy.size, 123);

assert.deepEqual(
  logged, ['GET size']);

Siehe die Referenz für die vollständige API für eine Liste der abfangbaren Operationen.

18.2 Programmierung versus Metaprogrammierung

Bevor wir uns damit beschäftigen, was Proxies sind und warum sie nützlich sind, müssen wir zunächst verstehen, was Metaprogrammierung ist.

In der Programmierung gibt es Ebenen.

Basis- und Metaebene können unterschiedliche Sprachen sein. Im folgenden Beispielprogramm ist die Metaprogrammiersprache JavaScript und die Basisprogrammiersprache Java.

const str = 'Hello' + '!'.repeat(3);
console.log('System.out.println("'+str+'")');

Metaprogrammierung kann verschiedene Formen annehmen. Im vorherigen Beispiel haben wir Java-Code auf die Konsole ausgegeben. Verwenden wir JavaScript sowohl als Metaprogrammiersprache als auch als Basisprogrammiersprache. Das klassische Beispiel hierfür ist die Funktion eval(), mit der wir JavaScript-Code im laufenden Betrieb auswerten/kompilieren können. In der folgenden Interaktion verwenden wir sie, um den Ausdruck 5 + 2 auszuwerten.

> eval('5 + 2')
7

Andere JavaScript-Operationen sehen vielleicht nicht wie Metaprogrammierung aus, sind es aber tatsächlich, wenn wir genauer hinsehen.

// Base level
const obj = {
  hello() {
    console.log('Hello!');
  },
};

// Meta level
for (const key of Object.keys(obj)) {
  console.log(key);
}

Das Programm untersucht seine eigene Struktur während der Ausführung. Das sieht nicht nach Metaprogrammierung aus, da die Trennung zwischen Programmierkonstrukten und Datenstrukturen in JavaScript verschwommen ist. Alle Object.*-Methoden können als Metaprogrammierungsfunktionalität betrachtet werden.

18.2.1 Arten der Metaprogrammierung

Reflektive Metaprogrammierung bedeutet, dass ein Programm sich selbst verarbeitet. Kiczales et al. [2] unterscheiden drei Arten der reflektiven Metaprogrammierung:

Betrachten wir Beispiele.

Beispiel: Introspektion. Object.keys() führt Introspektion durch (siehe vorheriges Beispiel).

Beispiel: Selbstmodifikation. Die folgende Funktion moveProperty verschiebt eine Eigenschaft von einer Quelle zu einem Ziel. Sie führt Selbstmodifikation durch den Klammeroperator für den Eigenschaftszugriff, den Zuweisungsoperator und den delete-Operator durch. (Im Produktionscode würden wir für diese Aufgabe wahrscheinlich Eigenschaftsdeskriptoren verwenden.)

function moveProperty(source, propertyName, target) {
  target[propertyName] = source[propertyName];
  delete source[propertyName];
}

So wird moveProperty() verwendet.

const obj1 = { color: 'blue' };
const obj2 = {};

moveProperty(obj1, 'color', obj2);

assert.deepEqual(
  obj1, {});

assert.deepEqual(
  obj2, { color: 'blue' });

ECMAScript 5 unterstützt keine Interzession; Proxies wurden geschaffen, um diese Lücke zu schließen.

18.3 Proxies erklärt

Proxies bringen Interzession nach JavaScript. Sie funktionieren wie folgt. Es gibt viele Operationen, die wir auf einem Objekt obj ausführen können – zum Beispiel:

Proxies sind spezielle Objekte, die es uns ermöglichen, einige dieser Operationen anzupassen. Ein Proxy wird mit zwei Parametern erstellt:

Hinweis: Die Verbform von „Interzession“ ist „interzedieren“. Interzession ist bidirektional. Abfangen ist unidirektional.

18.3.1 Ein Beispiel

Im folgenden Beispiel fängt der Handler die Operationen get und has ab.

const logged = [];

const target = {};
const handler = {
  /** Intercepts: getting properties */
  get(target, propKey, receiver) {
    logged.push(`GET ${propKey}`);
    return 123;
  },

  /** Intercepts: checking whether properties exist */
  has(target, propKey) {
    logged.push(`HAS ${propKey}`);
    return true;
  }
};
const proxy = new Proxy(target, handler);

Wenn wir eine Eigenschaft lesen (Zeile A) oder den in-Operator verwenden (Zeile B), fängt der Handler diese Operationen ab.

assert.equal(proxy.age, 123); // (A)
assert.equal('hello' in proxy, true); // (B)

assert.deepEqual(
  logged, [
    'GET age',
    'HAS hello',
  ]);

Der Handler implementiert den Trap set (Schreiben von Eigenschaften) nicht. Daher wird das Setzen von proxy.age an target weitergeleitet und führt dazu, dass target.age gesetzt wird.

proxy.age = 99;
assert.equal(target.age, 99);

18.3.2 Funktionsspezifische Traps

Wenn das Target eine Funktion ist, können zwei zusätzliche Operationen abgefangen werden:

Der Grund, warum diese Traps nur für Funktionsziele aktiviert sind, ist einfach: Andernfalls könnten wir die Operationen apply und construct nicht weiterleiten.

18.3.3 Abfangen von Methodenaufrufen

Wenn wir Methodenaufrufe über einen Proxy abfangen wollen, stehen wir vor einer Herausforderung: Es gibt keinen Trap für Methodenaufrufe. Stattdessen wird ein Methodenaufruf als eine Sequenz von zwei Operationen betrachtet:

Daher müssen wir, wenn wir Methodenaufrufe abfangen wollen, zwei Operationen abfangen:

Der folgende Code zeigt, wie das gemacht wird.

const traced = [];

function traceMethodCalls(obj) {
  const handler = {
    get(target, propKey, receiver) {
      const origMethod = target[propKey];
      return function (...args) { // implicit parameter `this`!
        const result = origMethod.apply(this, args);
        traced.push(propKey + JSON.stringify(args)
          + ' -> ' + JSON.stringify(result));
        return result;
      };
    }
  };
  return new Proxy(obj, handler);
}

Wir verwenden nicht einen Proxy für die zweite Abfangung; wir umschließen einfach die ursprüngliche Methode in einer Funktion.

Verwenden wir das folgende Objekt, um traceMethodCalls() auszuprobieren.

const obj = {
  multiply(x, y) {
    return x * y;
  },
  squared(x) {
    return this.multiply(x, x);
  },
};

const tracedObj = traceMethodCalls(obj);
assert.equal(
  tracedObj.squared(9), 81);

assert.deepEqual(
  traced, [
    'multiply[9,9] -> 81',
    'squared[9] -> 81',
  ]);

Sogar der Aufruf this.multiply() innerhalb von obj.squared() wird nachverfolgt! Das liegt daran, dass this weiterhin auf den Proxy verweist.

Das ist keine die effizienteste Lösung. Man könnte zum Beispiel Methoden cachen. Außerdem haben Proxies selbst Auswirkungen auf die Leistung.

18.3.4 Widerrufbare Proxies

Proxies können widerrufen (abgeschaltet) werden.

const {proxy, revoke} = Proxy.revocable(target, handler);

Nachdem wir die Funktion revoke zum ersten Mal aufgerufen haben, führt jeder Vorgang, den wir auf proxy anwenden, zu einem TypeError. Nachfolgende Aufrufe von revoke haben keine weiteren Auswirkungen.

const target = {}; // Start with an empty object
const handler = {}; // Don’t intercept anything
const {proxy, revoke} = Proxy.revocable(target, handler);

// `proxy` works as if it were the object `target`:
proxy.city = 'Paris';
assert.equal(proxy.city, 'Paris');

revoke();

assert.throws(
  () => proxy.prop,
  /^TypeError: Cannot perform 'get' on a proxy that has been revoked$/
);

18.3.5 Proxies als Prototypen

Ein Proxy proto kann zum Prototyp eines Objekts obj werden. Einige Operationen, die in obj beginnen, werden in proto fortgesetzt. Eine solche Operation ist get.

const proto = new Proxy({}, {
  get(target, propertyKey, receiver) {
    console.log('GET '+propertyKey);
    return target[propertyKey];
  }
});

const obj = Object.create(proto);
obj.weight;

// Output:
// 'GET weight'

Die Eigenschaft weight kann nicht in obj gefunden werden, weshalb die Suche in proto fortgesetzt wird und dort der Trap get ausgelöst wird. Es gibt weitere Operationen, die Prototypen beeinflussen; sie sind am Ende dieses Kapitels aufgeführt.

18.3.6 Weiterleitung abgefangener Operationen

Operationen, deren Traps der Handler nicht implementiert, werden automatisch an das Target weitergeleitet. Manchmal gibt es eine zusätzliche Aufgabe, die wir ausführen möchten, zusätzlich zur Weiterleitung der Operation. Zum Beispiel, um alle Operationen abzufangen und zu protokollieren, ohne dass sie das Target erreichen.

const handler = {
  deleteProperty(target, propKey) {
    console.log('DELETE ' + propKey);
    return delete target[propKey];
  },
  has(target, propKey) {
    console.log('HAS ' + propKey);
    return propKey in target;
  },
  // Other traps: similar
}
18.3.6.1 Verbesserung: Verwendung von Reflect.*

Für jeden Trap protokollieren wir zuerst den Namen der Operation und leiten sie dann weiter, indem wir sie manuell ausführen. JavaScript verfügt über das modulähnliche Objekt Reflect, das bei der Weiterleitung hilft.

Für jeden Trap

handler.trap(target, arg_1, ···, arg_n)

Reflect hat eine Methode.

Reflect.trap(target, arg_1, ···, arg_n)

Wenn wir Reflect verwenden, sieht das vorherige Beispiel wie folgt aus.

const handler = {
  deleteProperty(target, propKey) {
    console.log('DELETE ' + propKey);
    return Reflect.deleteProperty(target, propKey);
  },
  has(target, propKey) {
    console.log('HAS ' + propKey);
    return Reflect.has(target, propKey);
  },
  // Other traps: similar
}
18.3.6.2 Verbesserung: Implementierung des Handlers mit Proxy

Nun ist das, was jeder der Traps tut, so ähnlich, dass wir den Handler über einen Proxy implementieren können.

const handler = new Proxy({}, {
  get(target, trapName, receiver) {
    // Return the handler method named trapName
    return (...args) => {
      console.log(trapName.toUpperCase() + ' ' + args[1]);
      // Forward the operation
      return Reflect[trapName](...args);
    };
  },
});

Für jeden Trap fragt der Proxy über die get-Operation nach einer Handler-Methode und wir geben ihm eine. Das heißt, alle Handler-Methoden können über die einzelne Meta-Methode get implementiert werden. Es war eines der Ziele der Proxy-API, diese Art der Virtualisierung zu vereinfachen.

Verwenden wir diesen Proxy-basierten Handler.

const target = {};
const proxy = new Proxy(target, handler);

proxy.distance = 450; // set
assert.equal(proxy.distance, 450); // get

// Was `set` operation correctly forwarded to `target`?
assert.equal(
  target.distance, 450);

// Output:
// 'SET distance'
// 'GETOWNPROPERTYDESCRIPTOR distance'
// 'DEFINEPROPERTY distance'
// 'GET distance'

18.3.7 Fallstrick: Nicht alle Objekte können transparent von Proxies umschlossen werden

Ein Proxy-Objekt kann als Abfangen von Operationen auf seinem Target-Objekt betrachtet werden – der Proxy umschließt das Target. Das Handler-Objekt des Proxys ist wie ein Beobachter oder Listener für den Proxy. Es gibt an, welche Operationen abgefangen werden sollen, indem entsprechende Methoden (get zum Lesen einer Eigenschaft usw.) implementiert werden. Wenn die Handler-Methode für eine Operation fehlt, wird diese Operation nicht abgefangen. Sie wird einfach an das Target weitergeleitet.

Daher sollte der Proxy das Target transparent umschließen, wenn der Handler das leere Objekt ist. Leider funktioniert das nicht immer.

18.3.7.1 Das Umschließen eines Objekts beeinflusst this

Bevor wir tiefer eintauchen, wollen wir kurz wiederholen, wie das Umschließen eines Targets this beeinflusst.

const target = {
  myMethod() {
    return {
      thisIsTarget: this === target,
      thisIsProxy: this === proxy,
    };
  }
};
const handler = {};
const proxy = new Proxy(target, handler);

Wenn wir target.myMethod() direkt aufrufen, zeigt this auf target.

assert.deepEqual(
  target.myMethod(), {
    thisIsTarget: true,
    thisIsProxy: false,
  });

Wenn wir diese Methode über den Proxy aufrufen, zeigt this auf proxy.

assert.deepEqual(
  proxy.myMethod(), {
    thisIsTarget: false,
    thisIsProxy: true,
  });

Das heißt, wenn der Proxy einen Methodenaufruf an das Target weiterleitet, wird this nicht geändert. Infolgedessen bleibt der Proxy im Spiel, wenn das Target this verwendet, z. B. um einen Methodenaufruf zu tätigen.

18.3.7.2 Objekte, die nicht transparent umschlossen werden können

Normalerweise umschließen Proxies mit leeren Handlern Targets transparent: Wir bemerken nicht, dass sie da sind, und sie ändern das Verhalten der Targets nicht.

Wenn jedoch ein Target Informationen mit this über einen Mechanismus verknüpft, der nicht von Proxies gesteuert wird, haben wir ein Problem: Die Dinge schlagen fehl, weil unterschiedliche Informationen verknüpft sind, je nachdem, ob das Target umschlossen ist oder nicht.

Zum Beispiel speichert die folgende Klasse Person private Informationen in der WeakMap _name (mehr Informationen zu dieser Technik finden Sie in JavaScript for impatient programmers).

const _name = new WeakMap();
class Person {
  constructor(name) {
    _name.set(this, name);
  }
  get name() {
    return _name.get(this);
  }
}

Instanzen von Person können nicht transparent umschlossen werden.

const jane = new Person('Jane');
assert.equal(jane.name, 'Jane');

const proxy = new Proxy(jane, {});
assert.equal(proxy.name, undefined);

jane.name ist anders als das umschlossene proxy.name. Die folgende Implementierung hat dieses Problem nicht.

class Person2 {
  constructor(name) {
    this._name = name;
  }
  get name() {
    return this._name;
  }
}

const jane = new Person2('Jane');
assert.equal(jane.name, 'Jane');

const proxy = new Proxy(jane, {});
assert.equal(proxy.name, 'Jane');
18.3.7.3 Umschließen von Instanzen integrierter Konstruktoren

Instanzen der meisten integrierten Konstruktoren verwenden ebenfalls einen Mechanismus, der nicht von Proxies abgefangen wird. Daher können sie ebenfalls nicht transparent umschlossen werden. Das sehen wir, wenn wir eine Instanz von Date verwenden.

const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);

assert.throws(
  () => proxy.getFullYear(),
  /^TypeError: this is not a Date object\.$/
);

Der Mechanismus, der von Proxies unbeeinflusst bleibt, wird als interne Slots bezeichnet. Diese Slots sind eigenschaftsähnliche Speicher, die mit Instanzen verknüpft sind. Die Spezifikation behandelt diese Slots, als wären sie Eigenschaften mit Namen in eckigen Klammern. Zum Beispiel ist die folgende Methode intern und kann für alle Objekte O aufgerufen werden.

O.[[GetPrototypeOf]]()

Im Gegensatz zu Eigenschaften wird auf interne Slots nicht über normale „get“- und „set“-Operationen zugegriffen. Wenn .getFullYear() über einen Proxy aufgerufen wird, kann es den benötigten internen Slot nicht auf this finden und beschwert sich mit einem TypeError.

Für Date-Methoden besagt die Sprachspezifikation:

Sofern nicht ausdrücklich anders angegeben, sind die unten definierten Methoden des Date-Prototyp-Objekts nicht generisch und der this-Wert, der an sie übergeben wird, muss ein Objekt sein, das einen [[DateValue]]-internen Slot hat, der mit einem Zeitwert initialisiert wurde.

18.3.7.4 Eine Umgehungslösung

Als Umgehungslösung können wir ändern, wie der Handler Methodenaufrufe weiterleitet und this selektiv auf das Target und nicht auf den Proxy setzen.

const handler = {
  get(target, propKey, receiver) {
    if (propKey === 'getFullYear') {
      return target.getFullYear.bind(target);
    }
    return Reflect.get(target, propKey, receiver);
  },
};
const proxy = new Proxy(new Date('2030-12-24'), handler);
assert.equal(proxy.getFullYear(), 2030);

Der Nachteil dieses Ansatzes ist, dass keine der Operationen, die die Methode auf this ausführt, über den Proxy läuft.

18.3.7.5 Arrays können transparent umschlossen werden

Im Gegensatz zu anderen integrierten Objekten können Arrays transparent umschlossen werden.

const p = new Proxy(new Array(), {});

p.push('a');
assert.equal(p.length, 1);

p.length = 0;
assert.equal(p.length, 0);

Der Grund, warum Arrays umschließbar sind, ist, dass der Eigenschaftszugriff zwar angepasst wird, um .length funktionieren zu lassen, aber Array-Methoden nicht auf interne Slots angewiesen sind – sie sind generisch.

18.4 Anwendungsfälle für Proxies

Dieser Abschnitt zeigt, wofür Proxies verwendet werden können. Das gibt uns die Gelegenheit, die API in Aktion zu sehen.

18.4.1 Nachverfolgung von Zugritten auf Eigenschaften (get, set)

Nehmen wir an, wir haben eine Funktion tracePropertyAccesses(obj, propKeys), die protokolliert, wann eine Eigenschaft von obj, deren Schlüssel in der Array propKeys enthalten ist, gesetzt oder gelesen wird. Im folgenden Code wenden wir diese Funktion auf eine Instanz der Klasse Point an.

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  toString() {
    return `Point(${this.x}, ${this.y})`;
  }
}

// Trace accesses to properties `x` and `y`
const point = new Point(5, 7);
const tracedPoint = tracePropertyAccesses(point, ['x', 'y']);

Das Lesen und Setzen von Eigenschaften des nachverfolgten Objekts p hat folgende Auswirkungen:

assert.equal(tracedPoint.x, 5);
tracedPoint.x = 21;

// Output:
// 'GET x'
// 'SET x=21'

Interessanterweise funktioniert die Nachverfolgung auch dann, wenn Point auf die Eigenschaften zugreift, da this nun auf das nachverfolgte Objekt und nicht auf eine Instanz von Point verweist.

assert.equal(
  tracedPoint.toString(),
  'Point(21, 7)');

// Output:
// 'GET x'
// 'GET y'
18.4.1.1 Implementierung von tracePropertyAccesses() ohne Proxies

Ohne Proxies würden wir tracePropertyAccesses() wie folgt implementieren. Wir ersetzen jede Eigenschaft durch einen Getter und einen Setter, der die Zugriffe nachverfolgt. Die Setter und Getter verwenden ein zusätzliches Objekt, propData, um die Daten der Eigenschaften zu speichern. Beachten Sie, dass wir die ursprüngliche Implementierung destruktiv ändern, was bedeutet, dass wir Metaprogrammierung betreiben.

function tracePropertyAccesses(obj, propKeys, log=console.log) {
  // Store the property data here
  const propData = Object.create(null);

  // Replace each property with a getter and a setter
  propKeys.forEach(function (propKey) {
    propData[propKey] = obj[propKey];
    Object.defineProperty(obj, propKey, {
      get: function () {
        log('GET '+propKey);
        return propData[propKey];
      },
      set: function (value) {
        log('SET '+propKey+'='+value);
        propData[propKey] = value;
      },
    });
  });
  return obj;
}

Der Parameter log erleichtert das Unit-Testing dieser Funktion.

const obj = {};
const logged = [];
tracePropertyAccesses(obj, ['a', 'b'], x => logged.push(x));

obj.a = 1;
assert.equal(obj.a, 1);

obj.c = 3;
assert.equal(obj.c, 3);

assert.deepEqual(
  logged, [
    'SET a=1',
    'GET a',
  ]);
18.4.1.2 Implementierung von tracePropertyAccesses() mit einem Proxy

Proxies bieten uns eine einfachere Lösung. Wir fangen das Lesen und Setzen von Eigenschaften ab und müssen die Implementierung nicht ändern.

function tracePropertyAccesses(obj, propKeys, log=console.log) {
  const propKeySet = new Set(propKeys);
  return new Proxy(obj, {
    get(target, propKey, receiver) {
      if (propKeySet.has(propKey)) {
        log('GET '+propKey);
      }
      return Reflect.get(target, propKey, receiver);
    },
    set(target, propKey, value, receiver) {
      if (propKeySet.has(propKey)) {
        log('SET '+propKey+'='+value);
      }
      return Reflect.set(target, propKey, value, receiver);
    },
  });
}

18.4.2 Warnung vor unbekannten Eigenschaften (get, set)

Wenn es um den Zugriff auf Eigenschaften geht, ist JavaScript sehr nachsichtig. Wenn wir beispielsweise versuchen, eine Eigenschaft zu lesen und ihren Namen falsch schreiben, erhalten wir keine Ausnahme – wir erhalten das Ergebnis undefined.

Wir können Proxies verwenden, um in einem solchen Fall eine Ausnahme zu erhalten. Das funktioniert wie folgt. Wir machen den Proxy zum Prototyp eines Objekts. Wenn eine Eigenschaft nicht im Objekt gefunden wird, wird der get-Trap des Proxys ausgelöst.

Dies ist eine Implementierung dieses Ansatzes.

const propertyCheckerHandler = {
  get(target, propKey, receiver) {
    // Only check string property keys
    if (typeof propKey === 'string' && !(propKey in target)) {
      throw new ReferenceError('Unknown property: ' + propKey);
    }
    return Reflect.get(target, propKey, receiver);
  }
};
const PropertyChecker = new Proxy({}, propertyCheckerHandler);

Verwenden wir PropertyChecker für ein Objekt.

const jane = {
  __proto__: PropertyChecker,
  name: 'Jane',
};

// Own property:
assert.equal(
  jane.name,
  'Jane');

// Typo:
assert.throws(
  () => jane.nmae,
  /^ReferenceError: Unknown property: nmae$/);

// Inherited property:
assert.equal(
  jane.toString(),
  '[object Object]');
18.4.2.1 PropertyChecker als Klasse

Wenn wir PropertyChecker in einen Konstruktor umwandeln, können wir ihn über extends für Klassen verwenden.

// We can’t change .prototype of classes, so we are using a function
function PropertyChecker2() {}
PropertyChecker2.prototype = new Proxy({}, propertyCheckerHandler);

class Point extends PropertyChecker2 {
  constructor(x, y) {
    super();
    this.x = x;
    this.y = y;
  }
}

const point = new Point(5, 7);
assert.equal(point.x, 5);
assert.throws(
  () => point.z,
  /^ReferenceError: Unknown property: z/);

Dies ist die Prototypenkette von point.

const p = Object.getPrototypeOf.bind(Object);
assert.equal(p(point), Point.prototype);
assert.equal(p(p(point)), PropertyChecker2.prototype);
assert.equal(p(p(p(point))), Object.prototype);
18.4.2.2 Verhindern der versehentlichen Erstellung von Eigenschaften

Wenn wir uns Sorgen machen, versehentlich Eigenschaften zu erstellen, haben wir zwei Möglichkeiten:

18.4.3 Negative Array-Indizes (get)

Einige Array-Methoden erlauben es uns, auf das letzte Element über -1, auf das vorletzte Element über -2 usw. zu verweisen. Zum Beispiel:

> ['a', 'b', 'c'].slice(-1)
[ 'c' ]

Leider funktioniert das beim Zugriff auf Elemente über den Klammeroperator ([]) nicht. Wir können jedoch Proxies verwenden, um diese Funktionalität hinzuzufügen. Die folgende Funktion createArray() erstellt Arrays, die negative Indizes unterstützen. Sie tut dies, indem sie Proxies um Array-Instanzen wickelt. Die Proxies fangen die get-Operation ab, die durch den Klammeroperator ausgelöst wird.

function createArray(...elements) {
  const handler = {
    get(target, propKey, receiver) {
      if (typeof propKey === 'string') {
        const index = Number(propKey);
        if (index < 0) {
          propKey = String(target.length + index);
        }
      }
      return Reflect.get(target, propKey, receiver);
    }
  };
  // Wrap a proxy around the Array
  return new Proxy(elements, handler);
}
const arr = createArray('a', 'b', 'c');
assert.equal(
  arr[-1], 'c');
assert.equal(
  arr[0], 'a');
assert.equal(
  arr.length, 3);

18.4.4 Datenbindung (set)

Datenbindung befasst sich mit der Synchronisierung von Daten zwischen Objekten. Ein beliebter Anwendungsfall sind Widgets, die auf dem MVC (Model View Controller)-Muster basieren: Bei der Datenbindung bleibt die Ansicht (das Widget) aktuell, wenn wir das Modell (die vom Widget visualisierten Daten) ändern.

Um Datenbindung zu implementieren, müssen wir Änderungen an einem Objekt beobachten und darauf reagieren. Das folgende Code-Snippet ist eine Skizze, wie die Beobachtung von Änderungen für Arrays aussehen könnte.

function createObservedArray(callback) {
  const array = [];
  return new Proxy(array, {
    set(target, propertyKey, value, receiver) {
      callback(propertyKey, value);
      return Reflect.set(target, propertyKey, value, receiver);
    }
  });
}
const observedArray = createObservedArray(
  (key, value) => console.log(
    `${JSON.stringify(key)} = ${JSON.stringify(value)}`));
observedArray.push('a');

// Output:
// '"0" = "a"'
// '"length" = 1'

18.4.5 Zugriff auf einen RESTful-Webservice (Methodenaufrufe)

Ein Proxy kann verwendet werden, um ein Objekt zu erstellen, auf dem beliebige Methoden aufgerufen werden können. Im folgenden Beispiel erstellt die Funktion createWebService() ein solches Objekt, service. Das Aufrufen einer Methode auf service ruft den Inhalt der Ressource des Webservices mit demselben Namen ab. Der Abruf erfolgt über ein Promise.

const service = createWebService('http://example.com/data');
// Read JSON data in http://example.com/data/employees
service.employees().then((jsonStr) => {
  const employees = JSON.parse(jsonStr);
  // ···
});

Der folgende Code ist eine schnelle und schmutzige Implementierung von createWebService ohne Proxies. Wir müssen im Voraus wissen, welche Methoden auf service aufgerufen werden. Der Parameter propKeys liefert uns diese Information; er enthält ein Array mit Methodennamen.

function createWebService(baseUrl, propKeys) {
  const service = {};
  for (const propKey of propKeys) {
    service[propKey] = () => {
      return httpGet(baseUrl + '/' + propKey);
    };
  }
  return service;
}

Mit Proxies ist createWebService() einfacher.

function createWebService(baseUrl) {
  return new Proxy({}, {
    get(target, propKey, receiver) {
      // Return the method to be called
      return () => httpGet(baseUrl + '/' + propKey);
    }
  });
}

Beide Implementierungen verwenden die folgende Funktion, um HTTP-GET-Anfragen zu stellen (wie sie funktioniert, wird in JavaScript for impatient programmers erklärt).

function httpGet(url) {
  return new Promise(
    (resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.onload = () => {
        if (xhr.status === 200) {
          resolve(xhr.responseText); // (A)
        } else {
          // Something went wrong (404, etc.)
          reject(new Error(xhr.statusText)); // (B)
        }
      }
      xhr.onerror = () => {
        reject(new Error('Network error')); // (C)
      };
      xhr.open('GET', url);
      xhr.send();
    });
}

18.4.6 Widerrufbare Referenzen

Widerrufbare Referenzen funktionieren wie folgt: Ein Client darf nicht direkt auf eine wichtige Ressource (ein Objekt) zugreifen, sondern nur über eine Referenz (ein Zwischenobjekt, einen Wrapper um die Ressource). Normalerweise wird jede Operation, die auf die Referenz angewendet wird, an die Ressource weitergeleitet. Nachdem der Client fertig ist, wird die Ressource durch Widerrufen der Referenz, also durch Abschalten, geschützt. Von da an lösen die Anwendung von Operationen auf die Referenz Ausnahmen aus und nichts wird mehr weitergeleitet.

Im folgenden Beispiel erstellen wir eine widerrufbare Referenz für eine Ressource. Dann lesen wir eine der Eigenschaften der Ressource über die Referenz. Das funktioniert, da die Referenz uns den Zugriff gewährt. Als Nächstes widerrufen wir die Referenz. Jetzt lässt uns die Referenz die Eigenschaft nicht mehr lesen.

const resource = { x: 11, y: 8 };
const {reference, revoke} = createRevocableReference(resource);

// Access granted
assert.equal(reference.x, 11);

revoke();

// Access denied
assert.throws(
  () => reference.x,
  /^TypeError: Cannot perform 'get' on a proxy that has been revoked/
);

Proxies sind ideal geeignet für die Implementierung widerruflicher Referenzen, da sie Operationen abfangen und weiterleiten können. Dies ist eine einfache Proxy-basierte Implementierung von createRevocableReference.

function createRevocableReference(target) {
  let enabled = true;
  return {
    reference: new Proxy(target, {
      get(target, propKey, receiver) {
        if (!enabled) {
          throw new TypeError(
            `Cannot perform 'get' on a proxy that has been revoked`);
        }
        return Reflect.get(target, propKey, receiver);
      },
      has(target, propKey) {
        if (!enabled) {
          throw new TypeError(
            `Cannot perform 'has' on a proxy that has been revoked`);
        }
        return Reflect.has(target, propKey);
      },
      // (Remaining methods omitted)
    }),
    revoke: () => {
      enabled = false;
    },
  };
}

Der Code kann durch die Proxy-als-Handler-Technik aus dem vorherigen Abschnitt vereinfacht werden. Dieses Mal ist der Handler im Grunde das Reflect-Objekt. Somit gibt der get-Trap normalerweise die entsprechende Reflect-Methode zurück. Wenn die Referenz widerrufen wurde, wird stattdessen ein TypeError ausgelöst.

function createRevocableReference(target) {
  let enabled = true;
  const handler = new Proxy({}, {
    get(_handlerTarget, trapName, receiver) {
      if (!enabled) {
        throw new TypeError(
          `Cannot perform '${trapName}' on a proxy`
          + ` that has been revoked`);
      }
      return Reflect[trapName];
    }
  });
  return {
    reference: new Proxy(target, handler),
    revoke: () => {
      enabled = false;
    },
  };
}

Wir müssen widerrufbare Referenzen jedoch nicht selbst implementieren, da Proxies widerrufen werden können. Dieses Mal erfolgt der Widerruf im Proxy, nicht im Handler. Der Handler muss lediglich jede Operation an das Target weiterleiten. Wie wir gesehen haben, geschieht das automatisch, wenn der Handler keine Traps implementiert.

function createRevocableReference(target) {
  const handler = {}; // forward everything
  const { proxy, revoke } = Proxy.revocable(target, handler);
  return { reference: proxy, revoke };
}
18.4.6.1 Membranen

Membranen bauen auf der Idee der widerruflicher Referenzen auf: Bibliotheken zum sicheren Ausführen von nicht vertrauenswürdigem Code umschließen diesen Code mit einer Membran, um ihn zu isolieren und den Rest des Systems sicher zu halten. Objekte passieren die Membran in zwei Richtungen:

In beiden Fällen werden widerrufbare Referenzen um die Objekte gewickelt. Von gewickelten Funktionen oder Methoden zurückgegebene Objekte werden ebenfalls gewickelt. Zusätzlich wird, wenn ein gewickeltes nasses Objekt zurück in eine Membran übergeben wird, dieses entpackt.

Sobald der nicht vertrauenswürdige Code fertig ist, werden alle widerrufbaren Referenzen widerrufen. Infolgedessen kann kein Code von außen mehr ausgeführt werden, und externe Objekte, auf die er verweist, funktionieren ebenfalls nicht mehr. Der Caja Compiler ist „ein Werkzeug, um Drittanbieter-HTML, CSS und JavaScript sicher in Ihre Website einzubetten“. Er verwendet Membranen, um dieses Ziel zu erreichen.

18.4.7 Implementierung des DOM in JavaScript

Das Document Object Model (DOM) der Browser wird normalerweise als Mischung aus JavaScript und C++ implementiert. Die Implementierung in reinem JavaScript ist nützlich für:

Leider kann das Standard-DOM Dinge tun, die in JavaScript nicht einfach nachgebildet werden können. Zum Beispiel sind die meisten DOM-Kollektionen Live-Ansichten auf den aktuellen Zustand des DOM, die sich dynamisch ändern, wenn sich das DOM ändert. Infolgedessen sind reine JavaScript-Implementierungen des DOM nicht sehr effizient. Einer der Gründe für die Hinzufügung von Proxies zu JavaScript war, effizientere DOM-Implementierungen zu ermöglichen.

18.4.8 Weitere Anwendungsfälle

Es gibt weitere Anwendungsfälle für Proxies. Zum Beispiel:

18.4.9 Bibliotheken, die Proxies verwenden

18.5 Das Design der Proxy-API

In diesem Abschnitt gehen wir tiefer darauf ein, wie Proxies funktionieren und warum sie so funktionieren.

18.5.1 Schichtung: Trennung von Basis- und Metaebene

Firefox unterstützte eine Weile lang eine eingeschränkte Form der interzedierenden Metaprogrammierung: Wenn ein Objekt O eine Methode namens __noSuchMethod__ hatte, wurde es benachrichtigt, wenn eine Methode auf O aufgerufen wurde, die nicht existierte. Der folgende Code zeigt, wie das funktionierte.

const calc = {
  __noSuchMethod__: function (methodName, args) {
    switch (methodName) {
      case 'plus':
        return args.reduce((a, b) => a + b);
      case 'times':
        return args.reduce((a, b) => a * b);
      default:
        throw new TypeError('Unsupported: ' + methodName);
    }
  }
};

// All of the following method calls are implemented via
// .__noSuchMethod__().
assert.equal(
  calc.plus(3, 5, 2), 10);
assert.equal(
  calc.times(2, 3, 4), 24);

assert.equal(
  calc.plus('Parts', ' of ', 'a', ' string'),
  'Parts of a string');

Somit funktioniert __noSuchMethod__ ähnlich wie ein Proxy-Trap. Im Gegensatz zu Proxies ist der Trap eine eigene oder geerbte Methode des Objekts, dessen Operationen wir abfangen wollen. Das Problem bei diesem Ansatz ist, dass die Basisebene (normale Methoden) und die Metaebene (__noSuchMethod__) vermischt sind. Basisebenen-Code kann versehentlich eine Meta-Ebene-Methode aufrufen oder sehen und es besteht die Möglichkeit, versehentlich eine Meta-Ebene-Methode zu definieren.

Selbst in Standard-ECMAScript werden Basis- und Metaebene manchmal vermischt. Zum Beispiel können die folgenden Metaprogrammierungsmechanismen fehlschlagen, da sie sich auf der Basisebene befinden.

Bis jetzt sollte es offensichtlich sein, dass es problematisch ist, (Basis-Ebene) Eigenschaftsschlüssel speziell zu behandeln. Daher sind Proxies geschichtet: Basis-Ebene (das Proxy-Objekt) und Meta-Ebene (das Handler-Objekt) sind getrennt.

18.5.2 Virtuelle Objekte versus Wrapper

Proxies werden in zwei Rollen verwendet:

Ein früheres Design der Proxy-API sah Proxies als rein virtuelle Objekte vor. Es stellte sich jedoch heraus, dass selbst in dieser Rolle ein Target nützlich war, um Invarianten (die später erklärt werden) durchzusetzen und als Fallback für Traps zu dienen, die der Handler nicht implementiert.

18.5.3 Transparente Virtualisierung und Handler-Kapselung

Proxies sind auf zwei Arten abgeschirmt:

Beide Prinzipien verleihen Proxies erhebliche Macht, andere Objekte zu imitieren. Ein Grund für die Durchsetzung von Invarianten (wie später erklärt) ist es, diese Macht im Zaum zu halten.

Wenn wir eine Möglichkeit benötigen, Proxies von Nicht-Proxies zu unterscheiden, müssen wir sie selbst implementieren. Der folgende Code ist ein Modul lib.mjs, das zwei Funktionen exportiert: eine davon erstellt Proxies, die andere bestimmt, ob ein Objekt eines dieser Proxies ist.

// lib.mjs
const proxies = new WeakSet();

export function createProxy(obj) {
  const handler = {};
  const proxy = new Proxy(obj, handler);
  proxies.add(proxy);
  return proxy;
}

export function isProxy(obj) {
  return proxies.has(obj);
}

Dieses Modul verwendet die Datenstruktur WeakSet, um Proxies zu verfolgen. WeakSet ist für diesen Zweck ideal geeignet, da es seine Elemente nicht davon abhält, vom Garbage Collector eingesammelt zu werden.

Das nächste Beispiel zeigt, wie lib.mjs verwendet werden kann.

// main.mjs
import { createProxy, isProxy } from './lib.mjs';

const proxy = createProxy({});
assert.equal(isProxy(proxy), true);
assert.equal(isProxy({}), false);

18.5.4 Das Metaobjektprotokoll und Proxy-Traps

In diesem Abschnitt untersuchen wir, wie JavaScript intern strukturiert ist und wie die Menge der Proxy-Traps ausgewählt wurde.

Im Kontext von Programmiersprachen und API-Design ist ein Protokoll eine Menge von Schnittstellen plus Regeln für deren Verwendung. Die ECMAScript-Spezifikation beschreibt, wie JavaScript-Code ausgeführt wird. Sie enthält ein Protokoll zur Behandlung von Objekten. Dieses Protokoll operiert auf einer Metaebene und wird manchmal als Metaobjektprotokoll (MOP) bezeichnet. Das JavaScript MOP besteht aus eigenen internen Methoden, die alle Objekte besitzen. „Intern“ bedeutet, dass sie nur in der Spezifikation existieren (JavaScript-Engines mögen sie haben oder nicht) und nicht aus JavaScript zugänglich sind. Die Namen interner Methoden werden in doppelten eckigen Klammern geschrieben.

Die interne Methode zum Abrufen von Eigenschaften wird .[[Get]]() genannt. Wenn wir doppelte Unterstriche anstelle von doppelten Klammern verwenden, wäre diese Methode in JavaScript ungefähr wie folgt implementiert.

// Method definition
__Get__(propKey, receiver) {
  const desc = this.__GetOwnProperty__(propKey);
  if (desc === undefined) {
    const parent = this.__GetPrototypeOf__();
    if (parent === null) return undefined;
    return parent.__Get__(propKey, receiver); // (A)
  }
  if ('value' in desc) {
    return desc.value;
  }
  const getter = desc.get;
  if (getter === undefined) return undefined;
  return getter.__Call__(receiver, []);
}

Die in diesem Code aufgerufenen MOP-Methoden sind

In Zeile A sehen wir, warum Proxys in einer Prototypenkette von get erfahren, wenn eine Eigenschaft in einem „früheren“ Objekt nicht gefunden wird: Wenn keine eigene Eigenschaft mit dem Schlüssel propKey vorhanden ist, wird die Suche im Prototyp parent von this fortgesetzt.

Grundlegende versus abgeleitete Operationen. Wir können sehen, dass .[[Get]]() andere MOP-Operationen aufruft. Operationen, die dies tun, werden als abgeleitet bezeichnet. Operationen, die nicht von anderen Operationen abhängen, werden als grundlegend bezeichnet.

18.5.4.1 Das Metaobjektprotokoll von Proxys

Das Metaobjektprotokoll von Proxys unterscheidet sich von dem normaler Objekte. Bei normalen Objekten rufen abgeleitete Operationen andere Operationen auf. Bei Proxys wird jede Operation (unabhängig davon, ob sie grundlegend oder abgeleitet ist) entweder von einer Handler-Methode abgefangen oder an das Ziel weitergeleitet.

Welche Operationen sollten über Proxys abfangbar sein?

Der Vorteil der letzteren Vorgehensweise ist, dass sie die Leistung erhöht und bequemer ist. Wenn es beispielsweise keine Falle für get gäbe, müssten wir deren Funktionalität über getOwnPropertyDescriptor implementieren.

Ein Nachteil der Einbeziehung abgeleiteter Fallen ist, dass dies zu inkonsistentem Verhalten von Proxys führen kann. Beispielsweise kann get einen anderen Wert zurückgeben als der Wert im Deskriptor, der von getOwnPropertyDescriptor zurückgegeben wird.

18.5.4.2 Selektives Abfangen: Welche Operationen sollten abfangbar sein?

Das Abfangen durch Proxys ist selektiv: Wir können nicht jede Sprachoperation abfangen. Warum wurden einige Operationen ausgeschlossen? Betrachten wir zwei Gründe.

Erstens sind stabile Operationen nicht gut für das Abfangen geeignet. Eine Operation ist stabil, wenn sie für dieselben Argumente immer dieselben Ergebnisse liefert. Wenn ein Proxy eine stabile Operation abfangen kann, kann er instabil und damit unzuverlässig werden. Strikte Gleichheit (===) ist eine solche stabile Operation. Sie kann nicht abgefangen werden und ihr Ergebnis wird berechnet, indem der Proxy selbst als ein weiteres Objekt behandelt wird. Eine andere Möglichkeit, die Stabilität zu wahren, besteht darin, eine Operation auf das Ziel anstatt auf den Proxy anzuwenden. Wie später erläutert wird, wenn wir uns ansehen, wie Invarianten für Proxys erzwungen werden, geschieht dies, wenn Object.getPrototypeOf() auf einen Proxy angewendet wird, dessen Ziel nicht erweiterbar ist.

Ein zweiter Grund, nicht mehr Operationen abfangbar zu machen, ist, dass das Abfangen die Ausführung von benutzerdefiniertem Code in Situationen bedeutet, in denen dies normalerweise nicht möglich ist. Je mehr dieses Ineinandergreifen von Code stattfindet, desto schwieriger ist es, ein Programm zu verstehen und zu debuggen. Es beeinträchtigt auch die Leistung negativ.

18.5.4.3 Fallen: get versus invoke

Wenn wir virtuelle Methoden über Proxys erstellen möchten, müssen wir Funktionen aus einer get-Falle zurückgeben. Das wirft die Frage auf: Warum nicht eine zusätzliche Falle für Methodenaufrufe einführen (z. B. invoke)? Das würde es uns ermöglichen, zu unterscheiden zwischen

Dafür gibt es zwei Gründe.

Erstens unterscheiden nicht alle Implementierungen zwischen get und invoke. Beispielsweise Apple's JavaScriptCore tut dies nicht.

Zweitens sollte das Extrahieren einer Methode und deren späterer Aufruf über .call() oder .apply() denselben Effekt haben wie der Aufruf der Methode über Dispatch. Mit anderen Worten, die folgenden beiden Varianten sollten äquivalent funktionieren. Wenn es eine zusätzliche Falle invoke gäbe, wäre diese Äquivalenz schwerer aufrechtzuerhalten.

// Variant 1: call via dynamic dispatch
const result1 = obj.m();

// Variant 2: extract and call directly
const m = obj.m;
const result2 = m.call(obj);
18.5.4.3.1 Anwendungsfälle für invoke

Einige Dinge können nur getan werden, wenn wir zwischen get und invoke unterscheiden können. Diese Dinge sind daher mit der aktuellen Proxy-API unmöglich. Zwei Beispiele sind: Auto-Binding und das Abfangen fehlender Methoden. Untersuchen wir, wie sie implementiert würden, wenn Proxys invoke unterstützen würden.

Auto-Binding. Indem wir einen Proxy zum Prototyp eines Objekts obj machen, können wir Methoden automatisch binden

Auto-Binding hilft bei der Verwendung von Methoden als Callbacks. Beispielsweise wird Variante 2 aus dem vorherigen Beispiel einfacher

const boundMethod = obj.m;
const result = boundMethod();

Abfangen fehlender Methoden. invoke ermöglicht es einem Proxy, den zuvor erwähnten __noSuchMethod__-Mechanismus zu emulieren. Der Proxy wäre wieder der Prototyp eines Objekts obj. Er würde unterschiedlich reagieren, je nachdem, wie auf eine unbekannte Eigenschaft prop zugegriffen wird

18.5.5 Erzwingen von Invarianten für Proxys

Bevor wir uns ansehen, was Invarianten sind und wie sie für Proxys erzwungen werden, wollen wir überprüfen, wie Objekte durch Nicht-Erweiterbarkeit und Nicht-Konfigurierbarkeit geschützt werden können.

18.5.5.1 Objekte schützen

Es gibt zwei Möglichkeiten, Objekte zu schützen

Weitere Informationen zu diesem Thema finden Sie unter §10 „Schutz von Objekten vor Änderungen“.

18.5.5.2 Erzwingen von Invarianten

Traditionell sind Nicht-Erweiterbarkeit und Nicht-Konfigurierbarkeit

Diese und andere Eigenschaften, die angesichts von Sprachoperationen unverändert bleiben, werden als Invarianten bezeichnet. Es ist einfach, Invarianten über Proxys zu verletzen, da sie nicht intrinsisch durch Nicht-Erweiterbarkeit usw. gebunden sind. Die Proxy-API verhindert dies, indem sie das Zielobjekt und die Ergebnisse von Handler-Methoden prüft.

Die nächsten beiden Unterabschnitte beschreiben vier Invarianten. Eine vollständige Liste der Invarianten finden Sie am Ende dieses Kapitels.

18.5.5.3 Zwei Invarianten, die über das Zielobjekt erzwungen werden

Die folgenden beiden Invarianten beinhalten Nicht-Erweiterbarkeit und Nicht-Konfigurierbarkeit. Diese werden durch die Verwendung des Zielobjekts zur Buchführung erzwungen: Die von Handler-Methoden zurückgegebenen Ergebnisse müssen weitgehend mit dem Zielobjekt synchron sein.

18.5.5.4 Zwei Invarianten, die durch Prüfung von Rückgabewerten erzwungen werden

Die folgenden beiden Invarianten werden durch Prüfung von Rückgabewerten erzwungen

18.5.5.5 Vorteile von Invarianten

Das Erzwingen von Invarianten hat folgende Vorteile

Die nächsten beiden Abschnitte geben Beispiele für das Erzwingen von Invarianten.

18.5.5.6 Beispiel: Der Prototyp eines nicht erweiterbaren Ziels muss getreu dargestellt werden

Als Reaktion auf die getPrototypeOf-Falle muss der Proxy den Prototyp des Ziels zurückgeben, wenn das Ziel nicht erweiterbar ist.

Um diese Invariante zu demonstrieren, erstellen wir einen Handler, der einen anderen Prototyp als den des Ziels zurückgibt

const fakeProto = {};
const handler = {
  getPrototypeOf(t) {
    return fakeProto;
  }
};

Das Fälschen des Prototyps funktioniert, wenn das Ziel erweiterbar ist

const extensibleTarget = {};
const extProxy = new Proxy(extensibleTarget, handler);

assert.equal(
  Object.getPrototypeOf(extProxy), fakeProto);

Wir erhalten jedoch einen Fehler, wenn wir den Prototyp für ein nicht erweiterbares Objekt fälschen.

const nonExtensibleTarget = {};
Object.preventExtensions(nonExtensibleTarget);
const nonExtProxy = new Proxy(nonExtensibleTarget, handler);

assert.throws(
  () => Object.getPrototypeOf(nonExtProxy),
  {
    name: 'TypeError',
    message: "'getPrototypeOf' on proxy: proxy target is"
      + " non-extensible but the trap did not return its"
      + " actual prototype",
  });
18.5.5.7 Beispiel: Nicht schreibbare, nicht konfigurierbare Ziel-Eigenschaften müssen getreu dargestellt werden

Wenn das Ziel eine nicht schreibbare, nicht konfigurierbare Eigenschaft hat, muss der Handler deren Wert als Reaktion auf eine get-Falle zurückgeben. Um diese Invariante zu demonstrieren, erstellen wir einen Handler, der für Eigenschaften immer denselben Wert zurückgibt.

const handler = {
  get(target, propKey) {
    return 'abc';
  }
};
const target = Object.defineProperties(
  {}, {
    manufacturer: {
      value: 'Iso Autoveicoli',
      writable: true,
      configurable: true
    },
    model: {
      value: 'Isetta',
      writable: false,
      configurable: false
    },
  });
const proxy = new Proxy(target, handler);

Die Eigenschaft target.manufacturer ist weder nicht schreibbar noch nicht konfigurierbar, was bedeutet, dass der Handler einen anderen Wert vortäuschen darf

assert.equal(
  proxy.manufacturer, 'abc');

Die Eigenschaft target.model ist jedoch sowohl nicht schreibbar als auch nicht konfigurierbar. Daher können wir ihren Wert nicht fälschen

assert.throws(
  () => proxy.model,
  {
    name: 'TypeError',
    message: "'get' on proxy: property 'model' is a read-only and"
      + " non-configurable data property on the proxy target but"
      + " the proxy did not return its actual value (expected"
      + " 'Isetta' but got 'abc')",
  });

18.6 FAQ: Proxys

18.6.1 Wo ist die enumerate-Falle?

ECMAScript 6 hatte ursprünglich eine enumerate-Falle, die durch for-in-Schleifen ausgelöst wurde. Sie wurde jedoch kürzlich entfernt, um Proxys zu vereinfachen. Reflect.enumerate() wurde ebenfalls entfernt. (Quelle: TC39-Notizen)

18.7 Referenz: die Proxy-API

Dieser Abschnitt ist eine Kurzübersicht über die Proxy-API

Die Referenz verwendet den folgenden benutzerdefinierten Typ

type PropertyKey = string | symbol;

18.7.1 Proxys erstellen

Es gibt zwei Möglichkeiten, Proxys zu erstellen

18.7.2 Handler-Methoden

Dieser Unterabschnitt erklärt, welche Fallen von Handlern implementiert werden können und welche Operationen sie auslösen. Mehrere Fallen geben boolesche Werte zurück. Bei den Fallen has und isExtensible ist der boolesche Wert das Ergebnis der Operation. Bei allen anderen Fallen gibt der boolesche Wert an, ob die Operation erfolgreich war oder nicht.

Fallen für alle Objekte

Fallen für Funktionen (verfügbar, wenn das Ziel eine Funktion ist)

18.7.2.1 Grundlegende Operationen versus abgeleitete Operationen

Die folgenden Operationen sind grundlegend, sie verwenden keine anderen Operationen, um ihre Arbeit zu erledigen: apply, defineProperty, deleteProperty, getOwnPropertyDescriptor, getPrototypeOf, isExtensible, ownKeys, preventExtensions, setPrototypeOf

Alle anderen Operationen sind abgeleitet, sie können über grundlegende Operationen implementiert werden. Beispielsweise kann get durch Iteration über die Prototypenkette über getPrototypeOf und durch Aufruf von getOwnPropertyDescriptor für jedes Kettenmitglied implementiert werden, bis entweder eine eigene Eigenschaft gefunden wird oder die Kette endet.

18.7.3 Invarianten von Handler-Methoden

Invarianten sind Sicherheitsbeschränkungen für Handler. Dieser Unterabschnitt dokumentiert, welche Invarianten von der Proxy-API erzwungen werden und wie. Wenn wir unten lesen „der Handler muss X tun“, bedeutet dies, dass ein TypeError ausgelöst wird, wenn er es nicht tut. Einige Invarianten beschränken Rückgabewerte, andere Parameter. Die Korrektheit des Rückgabewerts einer Falle wird auf zwei Arten sichergestellt

Dies ist die vollständige Liste der erzwungenen Invarianten

  Invarianten in der ECMAScript-Spezifikation

In der Spezifikation sind die Invarianten in Abschnitt „Proxy Object Internal Methods and Internal Slots“ aufgeführt.

18.7.4 Operationen, die die Prototypenkette beeinflussen

Die folgenden Operationen normaler Objekte führen Operationen auf Objekten in der Prototypenkette durch. Wenn eines der Objekte in dieser Kette ein Proxy ist, werden seine Fallen ausgelöst. Die Spezifikation implementiert die Operationen als interne eigene Methoden (die für JavaScript-Code nicht sichtbar sind). In diesem Abschnitt tun wir jedoch so, als wären sie normale Methoden mit denselben Namen wie die Fallen. Der Parameter target wird zum Empfänger des Methodenaufrufs.

Alle anderen Operationen betreffen nur eigene Eigenschaften, sie haben keine Auswirkung auf die Prototypenkette.

  Interne Operationen in der ECMAScript-Spezifikation

In der Spezifikation werden diese (und andere) Operationen im Abschnitt „Ordinary Object Internal Methods and Internal Slots“ beschrieben.

18.7.5 Reflect

Das globale Objekt Reflect implementiert alle abfangbaren Operationen des JavaScript-Metaobjektprotokolls als Methoden. Die Namen dieser Methoden sind dieselben wie die der Handler-Methoden, was, wie wir gesehen haben, beim Weiterleiten von Operationen vom Handler zum Ziel hilfreich ist.

Mehrere Methoden haben boolesche Ergebnisse. Für .has() und .isExtensible() sind dies die Ergebnisse der Operation. Für die restlichen Methoden zeigen sie an, ob die Operation erfolgreich war.

18.7.5.1 Anwendungsfälle für Reflect neben der Weiterleitung

Abgesehen von der Weiterleitung von Operationen, warum ist Reflect nützlich [4]?

18.7.5.2 Object.* versus Reflect.*

Zukünftig wird Object Operationen hosten, die für normale Anwendungen von Interesse sind, während Reflect Operationen hosten wird, die auf niedrigerer Ebene angesiedelt sind.

18.8 Fazit

Damit schließen wir unseren ausführlichen Blick auf die Proxy-API. Eine Sache, die man beachten sollte, ist, dass Proxys Code verlangsamen. Das kann wichtig sein, wenn die Leistung kritisch ist.

Andererseits ist Leistung oft nicht entscheidend, und es ist schön, die Metaprogrammierungsfähigkeiten zu haben, die Proxys uns bieten.


Danksagungen

18.9 Weiterführende Lektüre