Kodokon kodokon.com

Proxy, Reflect et métaprogrammation

Interceptez les opérations fondamentales du langage avec les trappes de Proxy et déléguez proprement le comportement par défaut grâce à Reflect.

10 min · 3 questions

Ouvrir cette leçon dans Kodokon

Un Proxy enveloppe un objet cible et intercepte les opérations internes du langage via treize trappes : get, set, has (l'opérateur in), deleteProperty, ownKeys, apply, construct, etc. L'objet Reflect expose exactement les mêmes opérations sous forme de fonctions, avec les mêmes signatures que les trappes. C'est la fondation de la réactivité de Vue 3, des mocks dynamiques et des API de type ORM.

JAVASCRIPT
const target = { name: "Ada" };

const traced = new Proxy(target, {
  get(obj, prop, receiver) {
    console.log("get " + String(prop));
    return Reflect.get(obj, prop, receiver);
  }
});

traced.name; // get name
Trappe get minimale déléguée à Reflect.

Pourquoi Reflect.get(obj, prop, receiver) plutôt que obj[prop] ? À cause du receiver. Si la propriété est un accesseur (getter), Reflect.get l'invoque avec this pointant vers le receiver - le proxy ou l'objet qui en hérite - alors que obj[prop] fixerait this sur la cible brute. Sans receiver, les getters qui lisent this cassent dès qu'un objet hérite du proxy.

JAVASCRIPT
const base = {
  get label() {
    return this.name;
  }
};

const proxy = new Proxy(base, {
  get(target, prop, receiver) {
    return Reflect.get(target, prop, receiver);
  }
});

const child = Object.create(proxy);
child.name = "Ada";
console.log(child.label); // "Ada" grace au receiver
Le receiver préserve this dans les getters hérités.

Les cas d'usage sérieux : validation à l'écriture, propriétés virtuelles calculées à la volée, journalisation, contrôle d'accès (masquer les clés préfixées par _ via has et ownKeys), ou objets à comportement par défaut. Notez que Reflect.set renvoie un booléen au lieu de lever une erreur en mode strict : votre trappe doit propager cette valeur de retour.

JAVASCRIPT
function validated(target) {
  return new Proxy(target, {
    set(obj, prop, value, receiver) {
      if (prop === "age" && !Number.isInteger(value)) {
        throw new TypeError("age must be an integer");
      }
      return Reflect.set(obj, prop, value, receiver);
    }
  });
}

const user = validated({ age: 30 });
user.age = 31;       // ok
// user.age = "old"; // TypeError
Validation transparente à l'écriture.

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Dans une trappe get, pourquoi préférer Reflect.get(target, prop, receiver) à target[prop] ?
    • C'est plus rapide car Reflect court-circuite le prototype
    • Pour que les getters s'exécutent avec le bon this (le receiver) et non la cible brute
    • Parce que target[prop] relancerait la trappe et créerait une récursion infinie
    • Les deux formes sont strictement équivalentes
  2. Quelle trappe intercepte l'expression "id" in obj ?
    • get
    • has
    • ownKeys
    • getOwnPropertyDescriptor
  3. Une trappe get renvoie 42 pour une propriété de la cible qui vaut 1, déclarée non-configurable et non-writable. Que se passe-t-il ?
    • Le proxy renvoie 42, la trappe a toujours priorité
    • Le moteur renvoie silencieusement 1
    • Le moteur lève une TypeError pour violation d'invariant
    • Le comportement dépend du moteur JavaScript