Un objet Proxy encapsule un autre objet et intercepte des opĂ©rations, comme la lecture / Ă©criture de propriĂ©tĂ©s et dâautres, Ă©ventuellement en les manipulant de lui-mĂȘme ou en permettant Ă lâobjet de les gĂ©rer de maniĂšre transparente.
Les proxys sont utilisés dans de nombreuses bibliothÚques et certains frameworks de navigateur. Nous verrons de nombreux cas pratiques dans cet article.
Proxy
La syntaxe:
let proxy = new Proxy(target, handler)
target(cible) â est un objet Ă envelopper, cela peut ĂȘtre nâimporte quoi, y compris des fonctions.handlerâ configuration du proxy: un objet avec des âpiĂšgesâ qui interceptent les opĂ©rations. â par exemple.getpour lire une propriĂ©tĂ© detarget,setpour Ă©crire une propriĂ©tĂ© danstarget, etc.
Pour les opĂ©rations sur le proxy, sâil existe un piĂšge correspondant dans le handler, il sâexĂ©cute et le proxy a une chance de le gĂ©rer, sinon lâopĂ©ration est effectuĂ©e sur target.
Comme exemple de départ, créons un proxy sans aucun piÚge:
let target = {};
let proxy = new Proxy(target, {}); // handler vide
proxy.test = 5; // écrire dans proxy (1)
alert(target.test); // 5, la propriété est apparue dans target!
alert(proxy.test); // 5, nous pouvons aussi la lire Ă partir du proxy (2)
for(let key in proxy) alert(key); // test, les itérations fonctionne (3)
Comme il nây a pas de piĂšges, toutes les opĂ©rations sur le proxy sont transmises Ă target.
- Une opĂ©ration dâĂ©criture
proxy.test =définit la valeur surtarget. - Une opération de lecture
proxy.testrenvoie la valeur detarget. - LâitĂ©ration sur le
proxyrenvoie les valeurs detarget.
Comme nous pouvons le voir, sans aucun piĂšge, le proxy est un âwrapper transparentâ autour de target.
Le proxy est un âobjet exotiqueâ spĂ©cial. Il nâa pas de propriĂ©tĂ©s propres. Avec un handler vide, il transfĂšre de maniĂšre transparente les opĂ©rations vers target.
Pour activer plus de fonctionnalités, ajoutons des piÚges.
Que pouvons-nous intercepter avec eux?
Pour la plupart des opĂ©rations sur les objets, il existe une soi-disant âmĂ©thode interneâ dans la spĂ©cification JavaScript qui dĂ©crit comment cela fonctionne au plus bas niveau. Par exemple [[Get]], la mĂ©thode interne pour lire une propriĂ©tĂ©, [[Set]], la mĂ©thode interne pour Ă©crire une propriĂ©tĂ©, etc. Ces mĂ©thodes ne sont utilisĂ©es que dans la spĂ©cification, nous ne pouvons pas les appeler directement par leur nom.
Les piÚges proxy interceptent les invocations de ces méthodes. Ils sont répertoriés dans le Spécification du proxy et dans le tableau ci-dessous
Pour chaque mĂ©thode interne, il y a un piĂšge dans ce tableau: le nom de la mĂ©thode que nous pouvons ajouter au handler du new Proxy pour intercepter lâopĂ©ration:
| MĂ©thode interne | MĂ©thode dâhandler | Se dĂ©clenche lorsque⊠|
|---|---|---|
[[Get]] |
get |
lit une propriété |
[[Set]] |
set |
écrit une propriété |
[[HasProperty]] |
has |
utilise lâopĂ©rateur in |
[[Delete]] |
deleteProperty |
utilise lâopĂ©rateur delete |
[[Call]] |
apply |
appel une fonction |
[[Construct]] |
construct |
utilise lâopĂ©rateur new |
[[GetPrototypeOf]] |
getPrototypeOf |
Object.getPrototypeOf |
[[SetPrototypeOf]] |
setPrototypeOf |
Object.setPrototypeOf |
[[IsExtensible]] |
isExtensible |
Object.isExtensible |
[[PreventExtensions]] |
preventExtensions |
Object.preventExtensions |
[[DefineOwnProperty]] |
defineProperty |
Object.defineProperty, Object.defineProperties |
[[GetOwnProperty]] |
getOwnPropertyDescriptor |
Object.getOwnPropertyDescriptor, for..in, Object.keys/values/entries |
[[OwnPropertyKeys]] |
ownKeys |
Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object.keys/values/entries |
JavaScript applique certains invariants â conditions qui doivent ĂȘtre remplies par des mĂ©thodes et des piĂšges internes.
La plupart dâentre eux sont destinĂ©s aux valeurs de retour:
[[Set]]doit retournertruesi la valeur a Ă©tĂ© Ă©crite avec succĂšs, sinonfalse.[[Delete]]doit retournertruesi la valeur a Ă©tĂ© supprimĂ©e avec succĂšs, sinonfalse.- âŠet ainsi de suite, nous en verrons plus dans les exemples ci-dessous.
Il y a dâautres invariants, comme:
[[GetPrototypeOf]], appliquĂ© Ă lâobjet proxy doit renvoyer la mĂȘme valeur que[[GetPrototypeOf]]appliquĂ©e Ă lâobjet cible de lâobjet proxy. En dâautres termes, la lecture du prototype dâun proxy doit toujours renvoyer le prototype de lâobjet cible.
Les piÚges peuvent intercepter ces opérations, mais ils doivent suivre ces rÚgles.
Les invariants garantissent un comportement correct et cohérent des fonctionnalités du langage. La liste complÚte des invariants est dans la spécification. Vous ne les violerez probablement pas si vous ne faites pas quelque chose de bizarre.
Voyons comment cela fonctionne dans des cas pratiques.
Valeur par dĂ©faut avec le piĂšge âgetâ
Les piÚges les plus courants concernent les propriétés de lecture / écriture.
Pour intercepter la lecture, lâhandler doit avoir une mĂ©thode get (target, property, receiver).
Il se dĂ©clenche lorsquâune propriĂ©tĂ© est lue, avec les arguments suivants:
targetâ est lâobjet cible, celui passĂ© comme premier argument aunew proxy,propertyâ nom de la propriĂ©tĂ©,receiverâ si la propriĂ©tĂ© cible est un getter, lereceiverest lâobjet qui sera utilisĂ© commethisdans son appel. Habituellement, câest lâobjetproxylui-mĂȘme (ou un objet qui en hĂ©rite, si nous hĂ©ritons du proxy). Pour lâinstant, nous nâavons pas besoin de cet argument, il sera donc expliquĂ© plus en dĂ©tail plus tard.
Utilisons get pour implĂ©menter les valeurs par dĂ©faut dâun objet.
Nous allons créer un tableau numérique qui renvoie 0 pour les valeurs inexistantes.
Habituellement, quand on essaie dâobtenir un Ă©lĂ©ment de tableau non existant, il est undefined, mais nous encapsulerons un tableau normal dans le proxy qui interceptera la lecture et retournera 0 sâil nây a pas une telle propriĂ©tĂ©:
let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0; // valeur par défaut
}
}
});
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (élément inexistant)
Comme nous pouvons le voir, câest assez facile Ă faire avec un piĂšge get.
Nous pouvons utiliser Proxy pour implĂ©menter nâimporte quelle logique pour les valeurs âpar dĂ©fautâ.
Imaginez que nous ayons un dictionnaire, avec des phrases et leurs traductions:
let dictionary = {
'Hello': 'Hola',
'Bye': 'AdiĂłs'
};
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome'] ); // undefined
Ă lâheure actuelle, sâil nây a pas de phrase, la lecture de dictionary renvoie undefined. Mais en pratique, laisser une phrase non traduite est gĂ©nĂ©ralement mieux que undefined. Faisons donc renvoyer une phrase non traduite dans ce cas au lieu de undefined.
Pour y parvenir, nous allons envelopper le dictionary dans un proxy qui intercepte les opérations de lecture:
let dictionary = {
'Hello': 'Hola',
'Bye': 'AdiĂłs'
};
dictionary = new Proxy(dictionary, {
get(target, phrase) { // intercepter la lecture d'une propriété du dictionnaire
if (phrase in target) { // si nous l'avons dans le dictionnaire
return target[phrase]; // retourne la traduction
} else {
// sinon, retourne la phrase non traduite
return phrase;
}
}
});
// Rechercher des phrases arbitraires dans le dictionnaire!
// Au pire, ils ne sont pas traduits
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome to Proxy']); // Welcome to Proxy (pas de traduction)
Veuillez noter comment le proxy écrase la variable:
dictionary = new Proxy(dictionary, ...);
Le proxy doit remplacer totalement lâobjet cible partout. Personne ne devrait jamais rĂ©fĂ©rencer lâobjet cible aprĂšs quâil a Ă©tĂ© utilisĂ© comme target du proxy.
Validation avec le piĂšge âsetâ
Disons que nous voulons un tableau exclusivement pour les nombres. Si une valeur dâun autre type est ajoutĂ©e, il devrait y avoir une erreur.
Le piĂšge set se dĂ©clenche lorsquâune propriĂ©tĂ© est Ă©crite.
set(target, property, value, receiver):
targetâ est lâobjet cible, celui passĂ© comme premier argument aunew proxy,propertyâ nom de la propriĂ©tĂ©,valueâ valeur de la propriĂ©tĂ©,receiverâ similaire au piĂšgeget, ne concerne que les propriĂ©tĂ©s du setter.
Le piÚge set doit retourner true si le réglage est réussi et false dans le cas contraire (déclenche TypeError).
Utilisons-le pour valider de nouvelles valeurs:
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, val) { // intercepter l'écriture de propriété
if (typeof val == 'number') {
target[prop] = val;
return true;
} else {
return false;
}
}
});
numbers.push(1); // ajouté avec succÚs
numbers.push(2); // ajouté avec succÚs
alert("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError ('set' sur proxy retourne false)
alert("This line is never reached (error in the line above)");
Note: la fonctionnalité intégrée des tableaux fonctionne toujours! Les valeurs sont ajoutées par push. La propriété length augmente automatiquement lorsque des valeurs sont ajoutées. Notre proxy ne casse rien.
Nous nâavons pas Ă remplacer les mĂ©thodes de tableau Ă valeur ajoutĂ©e comme push et unshift, etc., pour y ajouter des vĂ©rifications, car en interne, elles utilisent lâopĂ©ration [[Set]] interceptĂ©e par le proxy.
Le code est donc propre et concis.
trueComme indiqué ci-dessus, il y a des invariants à tenir
Pour set, il doit retourner true pour une écriture réussie.
Si nous oublions de le faire ou retournons une valeur fausse, lâopĂ©ration dĂ©clenche TypeError.
ItĂ©ration avec âownKeysâ et âgetOwnPropertyDescriptorâ
La boucle Object.keys, for..in et la plupart des autres méthodes qui itÚrent sur les propriétés des objets utilisent la méthode interne [[OwnPropertyKeys]] (interceptée par le piÚge ownKeys) pour obtenir une liste des propriétés.
Ces méthodes diffÚrent dans les détails:
Object.getOwnPropertyNames(obj)renvoie des clĂ©s non symboliques.Object.getOwnPropertySymbols(obj)renvoie des clĂ©s symboliques.Object.keys/values()renvoie les clĂ©s / valeurs non symboliques avec lâindicateurenumerable(les indicateurs de propriĂ©tĂ© ont Ă©tĂ© expliquĂ©s dans lâarticle Attributs et descripteurs de propriĂ©tĂ©s).for..inboucle sur les clĂ©s non symboliques avec le drapeauenumerable, ainsi que sur les clĂ©s prototypes.
⊠Mais tous commencent par cette liste.
Dans lâexemple ci-dessous, nous utilisons le piĂšge ownKeys pour faire une boucle for..in sur user, ainsi que Object.keys et Object.values, pour ignorer les propriĂ©tĂ©s commençant par un trait de soulignement_ :
let user = {
name: "John",
age: 30,
_password: "***"
};
user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "ownKeys" filtre _password
for(let key in user) alert(key); // name, aprĂšs: age
// mĂȘme effet sur ces mĂ©thodes:
alert( Object.keys(user) ); // name,age
alert( Object.values(user) ); // John,30
JusquâĂ prĂ©sent, cela fonctionne.
Bien que, si nous renvoyons une clĂ© qui nâexiste pas dans lâobjet, Object.keys ne la rĂ©pertoriera pas:
let user = { };
user = new Proxy(user, {
ownKeys(target) {
return ['a', 'b', 'c'];
}
});
alert( Object.keys(user) ); // <empty>
Pourquoi? La raison est simple: Object.keys renvoie uniquement les propriĂ©tĂ©s avec lâindicateur enumerable. Pour le vĂ©rifier, il appelle la mĂ©thode interne [[GetOwnProperty]] pour chaque propriĂ©tĂ© Ă obtenir son descripteur. Et ici, comme il nây a pas de propriĂ©tĂ©, son descripteur est vide, pas dâindicateur enumerable, il est donc ignorĂ©.
Pour que Object.keys renvoie une propriĂ©tĂ©, nous avons besoin quâelle existe dans lâobjet, avec lâindicateur enumerable, ou nous pouvons intercepter les appels Ă [[GetOwnProperty]] (le piĂšge getOwnPropertyDescriptor le fait), et renvoyer un descripteur avec enumerable: true.
Voici un exemple:
let user = { };
user = new Proxy(user, {
ownKeys(target) { // appelé une fois pour obtenir une liste de propriétés
return ['a', 'b', 'c'];
},
getOwnPropertyDescriptor(target, prop) { // appelé pour chaque propriété
return {
enumerable: true,
configurable: true
/* ...other flags, probable "value:..." */
};
}
});
alert( Object.keys(user) ); // a, b, c
Notons encore une fois: nous nâavons besoin dâintercepter [[GetOwnProperty]] que si la propriĂ©tĂ© est absente dans lâobjet.
PropriĂ©tĂ©s protĂ©gĂ©es avec âdeletePropertyâ et autres piĂšges
Il existe une convention rĂ©pandue selon laquelle les propriĂ©tĂ©s et les mĂ©thodes prĂ©cĂ©dĂ©es dâun trait de soulignement _ sont internes. Ils ne doivent pas ĂȘtre accessibles depuis lâextĂ©rieur de lâobjet.
Techniquement, câest possible:
let user = {
name: "John",
_password: "secret"
};
alert(user._password); // secret
Utilisons des proxys pour empĂȘcher tout accĂšs aux propriĂ©tĂ©s commençant par _.
Nous aurons besoin des piĂšges:
getlancer une erreur lors de la lecture dâune telle propriĂ©tĂ©,setlancer une erreur lors de lâĂ©criture,deletePropertylancer une erreur lors de la suppression,ownKeyspour exclure les propriĂ©tĂ©s commençant par_defor..inet les mĂ©thodes commeObject.keys.
Voici le code:
let user = {
name: "John",
_password: "***"
};
user = new Proxy(user, {
get(target, prop) {
if (prop.startsWith('_')) {
throw new Error("Access denied");
}
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
},
set(target, prop, val) { // intercepter l'écriture de propriété
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
target[prop] = val;
return true;
}
},
deleteProperty(target, prop) { // pour intercepter la suppression de propriété
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
delete target[prop];
return true;
}
},
ownKeys(target) { // intercepter la liste des propriétés
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "get" ne permet pas de lire _password
try {
alert(user._password); // Erreur: accÚs refusé
} catch(e) { alert(e.message); }
// "set" ne permet pas d'écrire _password
try {
user._password = "test"; // Erreur: accÚs refusé
} catch(e) { alert(e.message); }
// "deleteProperty" ne permet pas de supprimer _password
try {
delete user._password; // Erreur: accÚs refusé
} catch(e) { alert(e.message); }
// "ownKeys" filtre _password
for(let key in user) alert(key); // name
Veuillez noter les détails importants dans le piÚge get, dans la ligne (*):
get(target, prop) {
// ...
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
}
Pourquoi avons-nous besoin dâune fonction pour appeler value.bind(target) ?
La raison est que les mĂ©thodes dâobjet, telles que user.checkPassword(), doivent pouvoir accĂ©der Ă _password:
user = {
// ...
checkPassword(value) {
// la méthode objet doit pouvoir lire _password
return value === this._password;
}
}
Lâappel user.checkPassword() obtient lâuser proxy comme this (lâobjet avant le point devient this), donc quand il essaie dâaccĂ©der Ă this._password, le piĂšge get sâactive (il se dĂ©clenche sur nâimporte quelle propriĂ©tĂ© lue) et gĂ©nĂšre une erreur.
Nous lions donc le contexte des mĂ©thodes objet Ă lâobjet dâorigine, target, dans la ligne (*). Ensuite, leurs futurs appels utiliseront target comme this, sans aucun piĂšge.
Cette solution fonctionne gĂ©nĂ©ralement, mais nâest pas idĂ©ale, car une mĂ©thode peut faire passer lâobjet non sollicitĂ© ailleurs.
En outre, un objet peut ĂȘtre proxy plusieurs fois (plusieurs procurations peuvent ajouter diffĂ©rents ârĂ©glagesâ Ă lâobjet), et si nous transmettons un objet non enveloppĂ© Ă une mĂ©thode, il peut y avoir des consĂ©quences inattendues.
Donc, un tel proxy ne devrait pas ĂȘtre utilisĂ© partout.
Les moteurs JavaScript modernes prennent en charge nativement les propriĂ©tĂ©s privĂ©es dans les classes, prĂ©fixĂ©es par #. Ils sont dĂ©crits dans lâarticle PropriĂ©tĂ©s et mĂ©thodes privĂ©es et protĂ©gĂ©es. Aucun proxy requis.
Ces propriétés ont cependant leurs propres problÚmes. En particulier, ils ne sont pas hérités.
âIn rangeâ avec le piĂšge âhasâ
Voyons plus dâexemples.
Nous avons un objet range:
let range = {
start: 1,
end: 10
};
Nous aimerions utiliser lâopĂ©rateur in pour vĂ©rifier quâun nombre est in range (Ă portĂ©e).
Le piĂšge has intercepte lâopĂ©rateur in.
has(target, property)
targetâ est lâobjet cible, passĂ© comme premier argument Ănew Proxy,propertyâ nom de la propriĂ©tĂ©
Voici la démo:
let range = {
start: 1,
end: 10
};
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end;
}
});
alert(5 in range); // true
alert(50 in range); // false
bon sucre syntaxique, non? Et trĂšs simple Ă mettre en Ćuvre.
Wrapping functions: "apply"
Nous pouvons Ă©galement envelopper un proxy autour dâune fonction.
Le piĂšge apply(target, thisArg, args) gĂšre lâappel dâun proxy en tant que fonction:
targetest lâobjet cible (la fonction est un objet en JavaScript),thisArgest la valeur dethis.argsest une liste dâarguments.
Par exemple, rappelons le dĂ©corateur delay(f, ms), que nous avons fait dans lâarticle DĂ©corateurs et transferts, call/apply.
Dans cet article, nous lâavons fait sans proxy. Un appel Ă delay(f, ms) a renvoyĂ© une fonction qui transfĂšre tous les appels Ă f aprĂšs ms millisecondes.
Voici lâimplĂ©mentation prĂ©cĂ©dente basĂ©e sur les fonctions:
function delay(f, ms) {
// retourner un wrapper qui passe l'appel à f aprÚs le délai d'expiration
return function() { // (*)
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
// aprÚs ce wrapping, les appels à sayHi seront retardés de 3 secondes
sayHi = delay(sayHi, 3000);
sayHi("John"); // Hello, John! (aprĂšs 3 secondes)
Comme nous lâavons dĂ©jĂ vu, cela fonctionne souvent. La fonction wrapper (*) effectue lâappel aprĂšs le dĂ©lai dâexpiration.
Mais une fonction wrapper ne transmet pas les opĂ©rations de lecture / Ă©criture de propriĂ©tĂ© ni rien dâautre. AprĂšs le wrapping, lâaccĂšs est perdu pour les propriĂ©tĂ©s des fonctions dâorigine, telles que le name, length et autres:
function delay(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
alert(sayHi.length); // 1 (la longueur de la fonction est le nombre d'arguments dans sa déclaration)
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 0 (dans la déclaration wrapper, il n'y a aucun argument)
Le proxy est beaucoup plus puissant, car il transmet tout Ă lâobjet cible.
Utilisons Proxy au lieu dâune fonction de âwrappingâ:
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 1 (*) le proxy transmet l'opération "get length" à la cible
sayHi("John"); // Hello, John! (aprĂšs 3 secondes)
Le rĂ©sultat est le mĂȘme, mais maintenant non seulement les appels, mais toutes les opĂ©rations sur le proxy sont transfĂ©rĂ©s vers la fonction dâorigine. Donc, sayHi.length est renvoyĂ© correctement aprĂšs le retour Ă la ligne (*).
Nous avons un wrapper âplus richeâ.
Dâautres piĂšges existent: la liste complĂšte se trouve au dĂ©but de cet article. Leur modĂšle dâutilisation est similaire Ă ce qui prĂ©cĂšde.
Reflect
Reflect est un objet intégré qui simplifie la création de Proxy.
Il a Ă©tĂ© dit prĂ©cĂ©demment que les mĂ©thodes internes, telles que [[Get]], [[Set]] et dâautres ne sont que des spĂ©cifications, elles ne peuvent pas ĂȘtre appelĂ©es directement.
Lâobjet Reflect rend cela possible. Ses mĂ©thodes sont des wrapper minimales autour des mĂ©thodes internes.
Voici des exemples dâopĂ©rations et dâappels Reflect identiques:
| Opération | Appel Reflect |
Méthode interne |
|---|---|---|
obj[prop] |
Reflect.get(obj, prop) |
[[Get]] |
obj[prop] = value |
Reflect.set(obj, prop, value) |
[[Set]] |
delete obj[prop] |
Reflect.deleteProperty(obj, prop) |
[[Delete]] |
new F(value) |
Reflect.construct(F, value) |
[[Construct]] |
| ⊠| ⊠| ⊠|
Par exemple:
let user = {};
Reflect.set(user, 'name', 'John');
alert(user.name); // John
Reflect nous permet dâappeler des opĂ©rateurs (new, delete âŠ) en tant que fonctions (Reflect.construct, Reflect.deleteProperty, âŠ). Câest une capacitĂ© intĂ©ressante, mais ici, une autre chose est importante.
Pour chaque mĂ©thode interne, piĂ©geable par Proxy, il existe une mĂ©thode correspondante dans Reflect, avec le mĂȘme nom et les mĂȘmes arguments que le piĂšge dans Proxy.
Nous pouvons donc utiliser Reflect pour transmettre une opĂ©ration Ă lâobjet dâorigine.
Dans cet exemple, les deux piĂšges get et set de maniĂšre transparente (comme si elles nâexistaient pas) transmettent les opĂ©rations de lecture / Ă©criture Ă lâobjet, affichant un message
let user = {
name: "John",
};
user = new Proxy(user, {
get(target, prop, receiver) {
alert(`GET ${prop}`);
return Reflect.get(target, prop, receiver); // (1)
},
set(target, prop, val, receiver) {
alert(`SET ${prop}=${val}`);
return Reflect.set(target, prop, val, receiver); // (2)
}
});
let name = user.name; // affiche "GET name"
user.name = "Pete"; // affiche "SET name=Pete"
Ici:
Reflect.getlit une propriĂ©tĂ© dâobjet.Reflect.setĂ©crit une propriĂ©tĂ© dâobjet et renvoietrueen cas de succĂšs,falsedans le cas contraire
Autrement dit, tout est simple: si un piĂšge veut renvoyer lâappel Ă lâobjet, il suffit dâappeler Reflect.<method> avec les mĂȘmes arguments.
Dans la plupart des cas, nous pouvons faire de mĂȘme sans Reflect, par exemple, la lecture dâune propriĂ©tĂ© Reflect.get(target, prop, receiver) peut ĂȘtre remplacĂ©e par target[prop]. Il y a cependant des nuances importantes.
Proxying a getter
Voyons un exemple qui montre pourquoi Reflect.get est meilleur. Et nous verrons Ă©galement pourquoi get/set a le troisiĂšme argument receiver, que nous nâavions pas utilisĂ© auparavant.
Nous avons un objet user avec la propriété _name et un getter pour cela.
Voici un proxy autour de lui:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop];
}
});
alert(userProxy.name); // Guest
Le piĂšge get est âtransparentâ ici, il renvoie la propriĂ©tĂ© dâorigine et ne fait rien dâautre. Cela suffit pour notre exemple.
Tout semble aller bien. Mais rendons lâexemple un peu plus complexe.
AprĂšs avoir hĂ©ritĂ© dâun autre objet admin de lâuser, nous pouvons observer le comportement incorrect:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop]; // (*) target = user
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
// Attendu: Admin
alert(admin.name); // retourne: Guest (?!?)
La lecture de admin.name devrait renvoyer "Admin", pas "Guest"!
Quel est le problĂšme? Peut-ĂȘtre que nous avons fait quelque chose de mal avec lâhĂ©ritage?
Mais si nous supprimons le proxy, tout fonctionnera comme prévu.
Le problĂšme est en fait dans le proxy, dans la ligne (*).
-
Lorsque nous lisons
admin.name, comme lâobjetadminnâa pas une telle propriĂ©tĂ©, la recherche va Ă son prototype. -
Le prototype est
userProxy. -
Lors de la lecture de la propriété
namedu proxy, son piĂšgegetse dĂ©clenche et la renvoie Ă partir de lâobjet dâorigine en tant quetarget[prop]dans la ligne(*).Un appel Ă
target[prop], lorsquepropest un getter, exĂ©cute son code dans le contextethis=target. Le rĂ©sultat est doncthis._namede lâobjettargetdâorigine , câest-Ă -dire de lâuser.
Pour rĂ©soudre de telles situations, nous avons besoin de receiver, le troisiĂšme argument du piĂšge get. Il garde le bon this Ă transmettre Ă un getter. Dans notre cas, câest admin.
Comment passer le contexte pour un getter? Pour une fonction rĂ©guliĂšre, nous pourrions utiliser call/apply, mais câest un getter, ce nâest pas âappelĂ©â, juste accessible.
Reflect.get peut faire ça. Tout fonctionnera bien si nous lâutilisons.
Voici la variante corrigée:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin
return Reflect.get(target, prop, receiver); // (*)
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
alert(admin.name); // Admin
Maintenant, receiver garde une rĂ©fĂ©rence Ă this correct (câest-Ă -dire admin), est transmis au getter en utilisant Reflect.get dans la ligne (*).
On peut réécrire le piÚge encore plus court:
get(target, prop, receiver) {
return Reflect.get(...arguments);
}
Les appels Reflect sont nommĂ©s exactement de la mĂȘme maniĂšre que les piĂšges et acceptent les mĂȘmes arguments. Ils ont Ă©tĂ© spĂ©cialement conçus de cette façon.
Donc, return Reflect... fournit un moyen sĂ»r et simple de faire avancer lâopĂ©ration et assure quâon oubliera rien.
Limitations du proxy
Les proxys offrent un moyen unique de modifier ou dâamĂ©liorer le comportement des objets existants au niveau le plus bas. Pourtant, ce nâest pas parfait. Il y a des limites.
Objets intégrés: emplacements internes
De nombreux objets intĂ©grĂ©s, par exemple Map, Set, Date, Promise et dâautres utilisent des «emplacements internes».
Ce sont des propriĂ©tĂ©s similaires, mais rĂ©servĂ©es Ă des fins internes uniquement. Par exemple, Map stocke les Ă©lĂ©ments dans lâemplacement interne [[MapData]]. Les mĂ©thodes intĂ©grĂ©es y accĂšdent directement, pas via les mĂ©thodes internes [[Get]]/[[Set]]. Donc, Proxy ne peut pas intercepter cela.
Pourquoi sâen soucier? Ils sont internes de toute façon!
Eh bien, voici le problĂšme. Une fois quâun objet intĂ©grĂ© comme celui-ci a Ă©tĂ© proxy, le proxy nâa pas ces emplacements internes, les mĂ©thodes intĂ©grĂ©es Ă©choueront donc.
Par exemple:
let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('test', 1); // Erreur
En interne, un Map stocke toutes les donnĂ©es dans son emplacement interne [[MapData]]. Le proxy nâa pas un tel emplacement. La mĂ©thode intĂ©grĂ©e Map.prototype.set essaie dâaccĂ©der Ă la propriĂ©tĂ© interne this.[[MapData]], mais parce que this=proxy, elle ne peut pas la trouver dans le proxy et Ă©choue.
Heureusement, il existe un moyen de le corriger:
let map = new Map();
let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
proxy.set('test', 1);
alert(proxy.get('test')); // 1 (ça fonctionne!)
Maintenant, cela fonctionne trĂšs bien, car le piĂšge get lie les propriĂ©tĂ©s de la fonction, telles que map.set, Ă lâobjet cible (map) lui-mĂȘme.
Contrairement Ă lâexemple prĂ©cĂ©dent, la valeur de this dans proxy.set(...) ne sera pas proxy, mais le map dâorigine. Ainsi, lorsque lâimplĂ©mentation interne de set essaie dâaccĂ©der Ă lâemplacement interne this.[[MapData]], il rĂ©ussit.
Array nâa pas dâemplacements internesUne exception notable: Array nâutilise pas dâemplacement internes. Pour des raisons historiques.
Il nây a donc pas de problĂšme de ce type lors de lâutilisation dâun proxy.
Champs privés
La mĂȘme chose se produit avec les champs de classe privĂ©s.
Par exemple, la mĂ©thode getName() accĂšde Ă la propriĂ©tĂ© privĂ©e #name et sâarrĂȘte aprĂšs le proxy:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {});
alert(user.getName()); // Erreur
La raison est que les champs privĂ©s sont implĂ©mentĂ©s Ă lâaide dâemplacement internes. JavaScript nâutilise pas [[Get]]/[[Set]] pour y accĂ©der.
Dans lâappel getName(), la valeur de this est lâuser proxy, et il nâa pas lâemplacement avec des champs privĂ©s.
Encore une fois, la solution avec la liaison de la méthode fonctionne:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
alert(user.getName()); // Guest
Cela dit, la solution prĂ©sente des inconvĂ©nients, comme expliquĂ© prĂ©cĂ©demment: elle expose lâobjet dâorigine Ă la mĂ©thode, ce qui peut potentiellement le faire passer plus loin et briser dâautres fonctionnalitĂ©s proxy.
Proxy != target
Le proxy et lâobjet dâorigine sont des objets diffĂ©rents. Câest normal, non?
Donc, si nous utilisons lâobjet dâorigine comme clĂ©, puis le proxy, le proxy ne peut pas ĂȘtre trouvĂ©:
let allUsers = new Set();
class User {
constructor(name) {
this.name = name;
allUsers.add(this);
}
}
let user = new User("John");
alert(allUsers.has(user)); // true
user = new Proxy(user, {});
alert(allUsers.has(user)); // false
Comme nous pouvons le voir, aprĂšs le proxy, nous ne pouvons pas trouver dâuser dans lâensemble allUsers, car le proxy est un objet diffĂ©rent.
Les proxys peuvent intercepter de nombreux opérateurs, tels que new (avec construct), in (avec has), delete (avec deleteProperty), etc.
Mais il nây a aucun moyen dâintercepter un test dâĂ©galitĂ© strict pour les objets. Un objet est strictement Ă©gal Ă lui-mĂȘme uniquement, et aucune autre valeur.
Ainsi, toutes les opĂ©rations et les classes intĂ©grĂ©es qui comparent les objets pour lâĂ©galitĂ© feront la diffĂ©rence entre lâobjet et le proxy. Pas de remplacement transparent ici.
Proxies révocables
Un proxy rĂ©vocable est un proxy qui peut ĂȘtre dĂ©sactivĂ©.
Disons que nous avons une ressource et que nous aimerions en fermer lâaccĂšs Ă tout moment.
Ce que nous pouvons faire, câest de lâenvelopper dans un proxy rĂ©vocable, sans aucun piĂšge. Un tel proxy transmettra les opĂ©rations Ă lâobjet, et nous pouvons le dĂ©sactiver Ă tout moment.
La syntaxe est:
let {proxy, revoke} = Proxy.revocable(target, handler)
Lâappel renvoie un objet avec la fonction proxy et revoke pour le dĂ©sactiver.
Voici un exemple:
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
// passer le proxy quelque part au lieu de l'objet...
alert(proxy.data); // Valuable data
// plus tard dans le code
revoke();
// le proxy ne fonctionne plus (révoqué)
alert(proxy.data); // Erreur
Un appel Ă revoke() supprime toutes les rĂ©fĂ©rences internes Ă lâobjet cible du proxy, de sorte quâils ne sont plus connectĂ©s.
Initialement, revoke est séparé de proxy, de sorte que nous pouvons passer proxy tout en laissant revoke dans la portée actuelle.
Nous pouvons également lier la méthode revoke au proxy en définissant proxy.revoke = revoke.
Une autre option est de créer une WeakMap qui a proxy comme clé et la valeur revoke correspondante, qui permet de trouver facilement revoke pour un proxy :
let revokes = new WeakMap();
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
revokes.set(proxy, revoke);
// ..plus tard dans notre code..
revoke = revokes.get(proxy);
revoke();
alert(proxy.data); // Erreur (révoqué)
Nous utilisons ici WeakMap au lieu de Map car cela ne bloquera pas le âgarbage collectionâ. Si un objet proxy devient âinaccessibleâ (par exemple si plus aucune variable ne le rĂ©fĂ©rence), WeakMap permet de lâeffacer de la mĂ©moire en mĂȘme temps que revoke dont nous nâaurons plus besoin.
Références
Résumé
Le proxy est un wrapper autour dâun objet, qui transfĂšre des opĂ©rations sur celui-ci Ă lâobjet, Ă©ventuellement en piĂ©geant certains dâentre eux.
Il peut envelopper nâimporte quel type dâobjet, y compris les classes et les fonctions.
La syntaxe est:
let proxy = new Proxy(target, {
/* traps */
});
⊠Ensuite, nous devrions utiliser le proxy partout au lieu de target. Un proxy nâa pas ses propres propriĂ©tĂ©s ou mĂ©thodes. Il intercepte une opĂ©ration si lâinterruption est fournie, sinon la transmet Ă target.
Nous pouvons piéger :
- Lecture (
get), Ă©criture (set), suppression (deleteProperty) dâune propriĂ©tĂ© (mĂȘme inexistante). - Appeler une fonction (piĂšge
apply). - LâopĂ©rateur
new(piĂšgeconstruct). - De nombreuses autres opĂ©rations (la liste complĂšte se trouve au dĂ©but de lâarticle et dans la documentation).
Cela nous permet de crĂ©er des propriĂ©tĂ©s et des mĂ©thodes âvirtuellesâ, dâimplĂ©menter des valeurs par dĂ©faut, des objets observables, des dĂ©corateurs de fonctions et bien plus encore.
Nous pouvons également envelopper un objet plusieurs fois dans différents proxys, en le décorant avec divers aspects de la fonctionnalité.
LâAPI de Reflect est conçu pour complĂ©ter Proxy. Pour tout piĂšge proxy, il existe un appel Reflect avec les mĂȘmes arguments. Nous devons les utiliser pour transfĂ©rer des appels vers des objets cibles
Les proxy ont certaines limites:
- Les objets intĂ©grĂ©s ont des âemplacements internesâ, lâaccĂšs Ă ceux-ci ne peut pas ĂȘtre proxy. Voir la solution de contournement ci-dessus.
- Il en va de mĂȘme pour les champs de classe privĂ©s, car ils sont implĂ©mentĂ©s en interne Ă lâaide de slots. Les appels de mĂ©thode proxy doivent donc avoir lâobjet cible comme
thispour y accĂ©der - Les tests dâĂ©galitĂ© strics
===ne peuvent pas ĂȘtre interceptĂ©s - Performances: les benchmarks dĂ©pendent dâun moteur, mais gĂ©nĂ©ralement accĂ©der Ă une propriĂ©tĂ© Ă lâaide dâun proxy simple prend un peu plus de temps. En pratique, cela nâa dâimportance que pour certains objets âbottleneckâ.
Commentaires
<code>, pour plusieurs lignes â enveloppez-les avec la balise<pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepenâŠ)