Retour au cours

frontend / javascript

Proxy et Reflect

Leçon 281 exercice

Explication

Ce que vous allez apprendre

  • Envelopper un objet dans un Proxy pour intercepter lecture, écriture et suppression
  • Nommer les traps principaux (get, set, has, deleteProperty) et leur opération native associée
  • Utiliser Reflect pour déléguer proprement au comportement par défaut dans un trap
  • Construire un cas d'usage concret : validation automatique ou valeur par défaut transparente
  • Relier ce mécanisme à la réactivité des frameworks modernes comme Vue

Dans quel contexte ?

Une équipe débogue un objet de configuration partagé entre plusieurs modules, où une valeur invalide (une chaîne au lieu d'un nombre pour port) finit par causer un crash très loin de l'endroit où elle a été assignée, rendant le bug difficile à tracer. En enveloppant l'objet de configuration dans un Proxy avec un trap set qui valide chaque écriture selon un schéma attendu, l'erreur est levée immédiatement à l'endroit exact de l'assignation fautive, au lieu de se propager silencieusement.

1. Des opérations normalement invisibles

Depuis le début du cours, vous manipulez des objets sans jamais avoir accès à ce qui se passe précisément quand une propriété est lue, écrite ou supprimée — ces opérations sont gérées silencieusement par le moteur.

Prérequis

Cette leçon suppose les objets et les classes (leçons 4 et 6) bien maîtrisés. Proxy est un outil avancé : il ne remplace pas les getters/setters de classe pour un usage courant.

2. Proxy rend ces opérations interceptables

Proxy change cela : il permet d'envelopper un objet dans un "intercepteur" qui peut observer, modifier, voire bloquer chacune de ces opérations avant qu'elles n'atteignent l'objet réel.

3. Un nom précis pour chaque opération

get intercepte la lecture d'une propriété, set l'écriture, has l'opérateur in, deleteProperty le delete. Chaque opération native a son "trap" dédié.

TrapOpération native interceptée
getLecture d'une propriété (obj.prop)
setÉcriture d'une propriété (obj.prop = x)
hasOpérateur in ("prop" in obj)
deletePropertySuppression (delete obj.prop)

4. Reflect : déléguer proprement au comportement par défaut

À l'intérieur de ces traps, Reflect fournit l'équivalent exact de chaque opérateur natif. C'est la manière recommandée de déléguer au comportement par défaut une fois votre logique exécutée, plutôt que de le réimplémenter à la main.

Piège fréquent

Un trap set qui oublie d'appeler Reflect.set(cible, propriete, valeur) (ou d'assigner manuellement cible[propriete] = valeur) et ne retourne pas true casse silencieusement l'écriture : la propriété semble modifiée dans votre logique, mais jamais réellement stockée sur l'objet cible.

5. Des cas d'usage concrets

Valider automatiquement chaque écriture selon un schéma, fournir une valeur par défaut transparente pour une propriété absente, ou tracer chaque accès à des fins de débogage.

6. Le cas d'usage le plus important pour la suite

C'est exactement ce mécanisme qui permet à des frameworks comme Vue de détecter qu'une donnée a changé et de déclencher un nouveau rendu, sans appel manuel à une fonction de mise à jour.

Proxy reste un outil puissant, à réserver aux cas où une interception réellement transparente est nécessaire — pas un remplacement systématique des getters/setters de classe vus à la leçon 4.

Commandes & code

Proxy et Reflect

Intercepter et personnaliser les opérations fondamentales sur un objet.

js
// --- Proxy de base : un objet "enveloppe" qui intercepte les operations ---
const cible = { nom: "Alice", age: 30 };

const gestionnaire = {
    get(objetCible, propriete, recepteur) {
        console.log(`Lecture de "${String(propriete)}"`);
        return Reflect.get(objetCible, propriete, recepteur);   // delegue au comportement par defaut
    },
    set(objetCible, propriete, valeur, recepteur) {
        console.log(`Ecriture de "${String(propriete)}" = ${valeur}`);
        return Reflect.set(objetCible, propriete, valeur, recepteur);
    },
};

const proxy = new Proxy(cible, gestionnaire);
proxy.nom;              // "Lecture de nom"
proxy.age = 31;           // "Ecriture de age = 31"
console.log(cible.age);    // 31 -- le proxy modifie bien l'objet cible sous-jacent

// --- Reflect : pendant "fonctionnel" des operateurs du langage, API coherente avec les traps ---
console.log(Reflect.has(cible, "nom"));            // equivalent a "nom" in cible
console.log(Reflect.ownKeys(cible));                  // equivalent a Object.getOwnPropertyNames + Symbols
console.log(Reflect.deleteProperty(cible, "age"));      // equivalent a delete cible.age

// --- Validation transparente : refuser certaines valeurs a l'ecriture ---
function creerObjetValide(schema) {
    return new Proxy({}, {
        set(objetCible, propriete, valeur) {
            const validateur = schema[propriete];
            if (validateur && !validateur(valeur)) {
                throw new TypeError(`Valeur invalide pour "${String(propriete)}" : ${valeur}`);
            }
            return Reflect.set(objetCible, propriete, valeur);
        },
    });
}
const utilisateur = creerObjetValide({
    age: (v) => typeof v === "number" && v >= 0,
    email: (v) => typeof v === "string" && v.includes("@"),
});
utilisateur.age = 25;             // OK
try {
    utilisateur.age = -5;
} catch (e) {
    console.error(e.message);
}

// --- has trap : proteger contre les fautes de frappe sur des proprietes "privees" par convention ---
const configurationSecurisee = new Proxy(
    { apiUrl: "https://api.example.com" },
    {
        has(objetCible, propriete) {
            if (typeof propriete === "string" && propriete.startsWith("_")) {
                return false;      // masque les proprietes internes du "in"
            }
            return Reflect.has(objetCible, propriete);
        },
        get(objetCible, propriete) {
            if (!(propriete in objetCible)) {
                throw new ReferenceError(`Propriete inconnue : "${String(propriete)}"`);
            }
            return Reflect.get(objetCible, propriete);
        },
    }
);
console.log(configurationSecurisee.apiUrl);
// console.log(configurationSecurisee.apiUrll);   // ReferenceError immediate, pas juste "undefined"

// --- Valeurs par defaut transparentes ---
function avecValeurParDefaut(objetSource, valeurDefaut) {
    return new Proxy(objetSource, {
        get(objetCible, propriete) {
            return propriete in objetCible ? objetCible[propriete] : valeurDefaut;
        },
    });
}
const compteurs = avecValeurParDefaut({}, 0);
console.log(compteurs.visites);       // 0, sans avoir jamais ete initialise

// --- apply trap : intercepter l'APPEL d'une fonction ---
function fonctionOriginale(a, b) {
    return a + b;
}
const fonctionTracee = new Proxy(fonctionOriginale, {
    apply(cibleFn, thisArg, args) {
        console.log(`Appel avec les arguments : ${args}`);
        const resultat = Reflect.apply(cibleFn, thisArg, args);
        console.log(`Resultat : ${resultat}`);
        return resultat;
    },
});
fonctionTracee(2, 3);

// --- construct trap : intercepter "new" sur une classe/fonction ---
class Produit {
    constructor(nom, prix) {
        this.nom = nom;
        this.prix = prix;
    }
}
const ProduitTrace = new Proxy(Produit, {
    construct(ClasseCible, args) {
        console.log(`Creation d'un Produit avec : ${args}`);
        return Reflect.construct(ClasseCible, args);
    },
});
const p = new ProduitTrace("Clavier", 49.99);

// --- deleteProperty trap : proteger certaines proprietes contre "delete" ---
const objetProtege = new Proxy(
    { id: 1, nom: "essentiel" },
    {
        deleteProperty(objetCible, propriete) {
            if (propriete === "id") {
                throw new Error("Impossible de supprimer la propriete 'id'");
            }
            return Reflect.deleteProperty(objetCible, propriete);
        },
    }
);
try {
    delete objetProtege.id;
} catch (e) {
    console.error(e.message);
}

// --- Proxy revocable : desactiver l'acces a un objet a la demande ---
const { proxy: acces, revoke } = Proxy.revocable({ secret: 42 }, {});
console.log(acces.secret);      // 42
revoke();
try {
    acces.secret;                  // TypeError : impossible d'utiliser un proxy revoque
} catch (e) {
    console.error(e.constructor.name);
}

// --- Cas d'usage reel : etat reactif minimal (base des frameworks type Vue) ---
function reactif(donnees, surChangement) {
    return new Proxy(donnees, {
        set(objetCible, propriete, valeur) {
            const ancienne = objetCible[propriete];
            const resultat = Reflect.set(objetCible, propriete, valeur);
            if (ancienne !== valeur) surChangement(propriete, valeur, ancienne);
            return resultat;
        },
    });
}
const etat = reactif({ compteur: 0 }, (prop, val, ancien) => {
    console.log(`"${prop}" change de ${ancien} a ${val} -- re-render declenche`);
});
etat.compteur++;

Résumé

  • Un Proxy intercepte les opérations fondamentales (get, set, has, deleteProperty, apply, construct...) sur un objet cible.
  • Reflect fournit l'équivalent fonctionnel de chaque opérateur natif, cohérent avec la signature des traps de Proxy.
  • Les traps permettent validation transparente, propriétés calculées, valeurs par défaut, ou traçage sans modifier l'objet original.
  • Proxy.revocable crée un proxy désactivable à la demande, utile pour révoquer un accès de façon sûre.

Exercices pratiques

1 disponible
1

Mission : réparer un Proxy de validation muet

Objectif : Corriger un trap set qui casse silencieusement les écritures, puis raisonner sur l'indépendance des traps entre eux.

Contexte

Une configuration validée par Proxy est écrite ainsi :

js
const config = new Proxy({}, {
    set(cible, propriete, valeur) {
        if (schema[propriete] && !schema[propriete](valeur)) {
            throw new TypeError("Valeur invalide");
        }
        cible[propriete] = valeur;
    },
});

config.port = 8080; // leve une TypeError, alors que 8080 est une valeur valide selon le schema

En mode strict (module ES ou classe), cette assignation échoue systématiquement, même pour une valeur parfaitement valide.

Résoudre l’exercice →