frontend / javascript
Proxy et Reflect
Explication
Ce que vous allez apprendre
- Envelopper un objet dans un
Proxypour intercepter lecture, écriture et suppression - Nommer les traps principaux (
get,set,has,deleteProperty) et leur opération native associée - Utiliser
Reflectpour 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é.
| Trap | Opération native interceptée |
|---|---|
get | Lecture d'une propriété (obj.prop) |
set | Écriture d'une propriété (obj.prop = x) |
has | Opérateur in ("prop" in obj) |
deleteProperty | Suppression (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.
// --- 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
Proxyintercepte les opérations fondamentales (get,set,has,deleteProperty,apply,construct...) sur un objet cible. Reflectfournit l'équivalent fonctionnel de chaque opérateur natif, cohérent avec la signature des traps deProxy.- Les traps permettent validation transparente, propriétés calculées, valeurs par défaut, ou traçage sans modifier l'objet original.
Proxy.revocablecrée un proxy désactivable à la demande, utile pour révoquer un accès de façon sûre.
Exercices pratiques
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 :
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 schemaEn mode strict (module ES ou classe), cette assignation échoue systématiquement, même pour une valeur parfaitement valide.