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â¦)