L’objet Proxy est souvent méconnu de la plupart des développeurs JavaScript. Celui-ci permet pourtant de résoudre facilement certains problèmes et de faire ce que l’on appelle de la métaprogrammation.
Qu’est-ce que la métaprogrammation ?
Avant de présenter l’objet Proxy
et son utilité, voyons qu’est-ce que la métaprogrammation, comme d’habitude demandons à Wikipédia :
La métaprogrammation, nommée par analogie avec les métadonnées et les métaclasses, désigne l’écriture de programmes qui manipulent des données décrivant elles-mêmes des programmes. Dans le cas particulier où le programme manipule ses propres instructions pendant son exécution, on parle de programme automodifiant.
Super, mais on n’a rien compris. Bon, je vais tenter de vous expliquer. Tout d’abord, savez-vous ce que signifie le préfixe « méta » ? Celui-ci exprime l’idée d’autoréférence, ainsi une métadonnée est une donnée décrivant une autre donnée, un métalangage est un langage décrivant un autre langage, etc.
C’est ce que nous dit la définition, la métaprogrammation est en gros un programme qui manipule un autre programme. Un exemple très simple est le cas d’un compilateur ou d’un interpréteur qui est un programme qui va manipuler votre code ou c’est encore le cas des debuggers.
Mais cela peut-être également, et c’est ce qui nous intéresse ici, la capacité à votre code d’agir sur lui-même et de modifier certains comportements prévus par le langage.
L’objet Proxy
Vous l’aurez deviné, l’objet Proxy
va nous permettre de faire de la métaprogrammation en JavaScript.
Comment ça marche ?
L’objet Proxy
permet d’encapsuler un autre objet et intercepter certaines opérations comme l’accès à une propriété, l’affectation d’une valeur à une propriété, l’appel d’une fonction, etc.
Pour créer un objet Proxy
rien de plus simple :
1 | const proxy = new Proxy(target, handler); |
Avec :
target
: Il s’agit de l’objet à envelopper. Cela peut être n’importe quel objet : une fonction, un tableau ou même un autre proxy;handler
: C’est l’objet qui définit la configuration du proxy. C’est ici que l’on mettra nos fonctions qui intercepteront les opérations sur notre objet comme nous le verrons juste après.
Prenons un exemple tout simple :
1 2 3 4 5 6 7 | const target = {}; const proxy = new Proxy(target, {}); proxy.test = 'test'; console.log(proxy.test); // test console.log(target.test); // test |
On remarque que l’ajout d’une propriété sur l’objet proxy est répercutée sur l’objet qu’il encapsule.
Bon ok c’est bien joli tout ça, mais ça ne sert pas à grand-chose pour le moment. On va donc voir comment configurer notre proxy avec le paramètre handler
pour intercepter les opérations sur notre objet.
It’s a trap !
Le paramètre handler
permet de définir ce que la norme appelle des trappes (ou traps en anglais) pour intercepter les opérations sur l’objet qu’encapsule le proxy. Ces trappes sont au nombre de treize et se déclenchent lors de certaines actions :
get
: lorsque l’on accède à la valeur d’une propriété;set
: lorsque l’on affecte une valeur à une propriété;deleteProperty
: lorsque l’on supprime une propriété via l’opérateurdelete
;has
: lorsque l’on fait appel à l’opérateurin
;apply
: lors de l’appel d’une fonction;construct
: Lorsque l’on créer un nouvel objet via l’opérateurnew
;getPrototypeOf
: Lors de l’appel à la méthodeObject.getPrototypeOf
;setPrototypeOf
: Lors de l’appel à la méthodeObject.setPrototypeOf
;ownKeys
: Lors de l’appel aux méthodesObject.getOwnPropertyNames
,Object.getOwnPropertySymbols
,Object.key
,Object.values
,Object.entries
ou à l’utilisation de la bouclefor..in
;isExtensible
: Lors de l’appel à la méthodeObject.isExtensible
;preventExtensions
: Lors de l’appel à la méthodeObject.preventExtensions
;getOwnPropertyDescriptor
: Lors de l’appel aux méthodesObject.getOwnPropertyDescriptor
,Object.key
,Object.values
,Object.entries
ou à l’utilisation de la bouclefor..in
;defineProperty
: Lors de l’appel aux méthodesObject.defineProperty
etObject.defineProperties
.
Bon, j’espère que vous êtes toujours là ! N’ayez pas peur on va surtout s’intéresser aux trappes get
et set
et croyez-moi il y a de quoi faire.
la trappe get
La trappe get
est appelée lorsque l’on souhaite accéder à une propriété d’un objet. La signature de la fonction est la suivante :
1 | get(target, property, receiver) |
Avec :
target
: L’objet que le proxy enveloppe;property
: Le nom de la propriété à laquelle on souhaite accéder;receiver
: Le proxy ou un objet qui en hérite.
Cette méthode peut renvoyer n’importe quelle valeur.
On va commencer par un exemple tout simple pour mieux comprendre le fonctionnement :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // On crée un objet avec une propriété "value" const obj = { value: 1 }; // On crée la configuration de notre proxy avec une trappe "get" const handlers = { get(target, property, receiver) { console.log('Access to property :', property); // On retourne la valeur de la propriété de notre objet encapsulé (target = obj) return target[property]; } }; // On crée le proxy de notre objet const proxyObj = new Proxy(obj, handlers); // On accède la propriété value proxyObj.value; // Access to property : value proxyObj['value']; // Access to property : value |
Dès que l’on accède à la propriété value
de notre objet proxyObj
, la trappe get
se déclenche, et nous retournons la valeur de la propriété value
via l’instruction return target[property]
.
Avant de voir des utilisations concrètes de la trappe get
, voyons à quoi sert le paramètre receiver
.
Reprenons notre exemple et ajoutons un getter val
pour récupérer la propriété value
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | const obj = { value: 1, get val() { return this.value; } }; const handlers = { get(target, property, receiver) { return target[property]; } }; const proxyObj = new Proxy(obj, handlers); |
Créons ensuite un second objet qui « hérite » de notre proxy via la chaîne de prototype. Je vous invite à aller lire l’article sur l’héritage multiple qui fait un rappel sur l’héritage en JavaScript.
1 2 3 4 5 | // On "hérite" du proxy const inheritsObj = Object.create(proxyObj); // On change la valeur de la propriété value inheritsObj.value = 2; |
Récupérons la propriété value
via le getter val
:
1 | console.log(inheritsObj.val); // 1 |
Et là ça coince, on récupère la valeur 1 au lieu de 2.
Le souci se situe dans notre trappe get
, lors du renvoi de la valeur target[property]
. Le paramètre property
est le nom de notre getter, de ce fait l’instruction target[property]
exécute le code de celui-ci. Vu que le paramètre target
est notre objet obj
cela retourne donc la valeur 1 et non 2, car la valeur this
au sein du getter est bien obj
et non inheritsObj
. Je vous invite à aller lire mon article sur this
si vous avez du mal à comprendre.
Comment passer la bonne valeur de this
au getter val
? Et bien c’est là que rentre en jeu le paramètre reveiver
. En effet, nous avons vu que ce paramètre était soit le proxy soit un objet qui en hérite, c’est bien notre cas ici, inheritsObj
hérite du proxy proxyObj
.
Malheureusement, nous ne pouvons pas directement récupérer la propriété de cette façon reveiver[property]
, car cela rappellerait la trappe get
et créerait une « boucle infinie » d’appels.
Pour remédier à ce problème, JavaScript propose un objet Reflect
qui fournit des méthodes permettant d’interagir avec les objets et faire appel aux fonctions internes du langage. Pour chaque trappe de l’objet Proxy
, il existe une méthode correspondante pour l’objet Reflect
.
Prenons celle qui nous intéresse :
1 | Reflect.get(target, property, receiver); |
Cette méthode permet de récupérer la valeur de la propriété d’un objet (c’est en fait un accesseur de propriété, comme .
ou []
mais sous la forme d’une fonction).
Les paramètres de cette méthode sont les suivants :
target
: L’objet sur lequel on souhaite récupérer la propriété;property
: Le nom de la propriété;receiver
: La valeur dethis
qui sera passée dans le cas de l’utilisation d’un getter;
Corrigeons notre exemple :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | const obj = { value: 1, get val() { return this.value; }, }; const handlers = { get(target, property, receiver) { return Reflect.get(target, property, receiver); }, }; const proxyObj = new Proxy(obj, handlers); const inheritsObj = Object.create(proxyObj); inheritsObj.value = 2; console.log(inheritsObj.val); // 2 |
Et voilà ! On obtient bien le résultat attendu.
Bon maintenant qu’on a toutes les informations qu’il nous fallait pour comprendre comment fonctionne l’objet Proxy
on va passer aux choses sérieuses et voir des cas d’utilisations.
Cas d’utilisation
On va voir plusieurs cas d’utilisation de la trappe get
, avec des exemples variés afin de voir les possibilités qui s’offrent à nous.
Renvoyer une erreur si une propriété n’existe pas
Lorsque l’on souhaite accéder à une propriété inexistante d’un objet, la valeur undefined
est renvoyée :
1 2 3 4 5 | const obj = { value: 1 }; console.log(obj.a); // undefined |
Il est possible via l’utilisation d’un proxy de renvoyer une erreur si la propriété n’existe pas :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | const existenceChecker = (obj) => new Proxy(obj, { get(target, property, receiver) { // On vérifie que la propriété existe au sein de l'objet if (!(property in target)) { // Si ce n'est pas le cas on lève une exception throw new ReferenceError(`Unknown property ${property}`); } // Sinon on renvoie la valeur de la propriété return Reflect.get(target, property, receiver); } }); const obj = { value: 1 }; // On encapsule l'objet avec notre proxy const objProxy = existenceChecker(obj); console.log(objProxy.value); // 1 console.log(objProxy.a); // ReferenceError: Unknown property a |
Calculer le temps d’exécution des méthodes
On a souvent besoin de calculer le temps d’exécution de nos méthodes. L’utilisation d’un proxy permet de réaliser cette opération de façon très simple et élégante :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | const benchmarkMethods = (obj) => new Proxy(obj, { get(target, property, receiver) { // On récupère la valeur de notre propriété const targetValue = Reflect.get(target, property, receiver); // On vérifie si la propriété est une fonction if (typeof targetValue === 'function') { let fn; // On vérifie si cette fonction est asynchrone if (targetValue.constructor.name === 'AsyncFunction') { // Si c'est le cas, on créer une nouvelle fonction asynchrone qui encapsule la méthode de l'objet fn = async function(...args) { console.time(`${property}`); // On exécute la méthode de l'objet et on attend la fin de son exécution const result = await targetValue.apply(this, args); console.timeEnd(`${property}`); return result; }; } else { // Sinon on créer simplement une fonction qui encapsule la méthode de l'objet fn = function(...args) { console.time(`${property}`); // On exécute la méthode de l'objet const result = targetValue.apply(this, args); console.timeEnd(`${property}`); return result; }; } return fn; } else { // Dans le cas où ce n'est pas une fonction on retourne simplement la valeur return targetValue; } } }); const obj = { fn() { /* Process */ }, async asyncFn() { return new Promise((resolve, reject) => setTimeout(resolve, 2000)); } }; const objProxy = benchmarkMethods(obj); objProxy.fn(); // fn: 0.604ms objProxy.asyncFn(); // asyncFn: 2.001s |
Espionner nos méthodes
Lorsque l’on fait des tests unitaires, on a souvent besoin de récupérer des informations sur l’exécution des méthodes de nos objets, que ce soit le nombre de fois où celles-ci ont été appelées ou les arguments passés en paramètres. On utilise pour cela ce que l’on appelle un spy. C’est d’ailleurs ce que propose la librairie sinon.
Voyons voir comment, à l’aide d’un proxy, réaliser un spy qui va compter le nombre de fois où nos méthodes ont été appelées :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | const spy = (obj) => { // L'objet pour stocker le nombre d'appels pour chaque méthode const countCall = {}; const proxy = new Proxy(obj, { get(target, property, receiver) { // On récupère la valeur de notre propriété const targetValue = Reflect.get(target, property, receiver); // Si la propriété n'est une fonction on retourne la valeur if (typeof targetValue !== 'function') { return targetValue; } // Sinon on renvoie une fonction qui encapsule la méthode de l'objet return function(...args) { // Si c'est la première fois que la méthode est appelé on initialise son compteur à 0 if (!countCall[property]) { countCall[property] = 0; } // On incrémente le nombre d'appels pour la fonction countCall[property]++; // On exécute la fonction et on renvoie son resultat return targetValue.apply(receiver, args); }; } }); // On ajoute une méthode à notre objet proxy permettant de récupérer le nombre d'appels pour une méthode proxy.calledCount = (methodName) => countCall[methodName] || 0; return proxy; }; const obj = { test() { } }; const spyObj = spy(obj); spyObj.test(); console.log(spyObj.calledCount('test')); // 1 spyObj.test(); spyObj.test(); console.log(spyObj.calledCount('test')); // 3 |
Encore une fois, quand on a compris le concept de proxy, c’est relativement simple. L’avantage de l’utilisation d’un proxy est que l’on a aucune modification à faire sur notre objet.
Un conteneur d’injection de dépendances
Pour ce dernier exemple de la trappe get
, on va voir comment à l’aide d’un proxy, créer simplement un conteneur d’injection de dépendances. J’ai fait un article sur l’injection de dépendances, je vous conseille d’aller y jeter un coup d’œil.
Prenons un exemple de trois services :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class Logger { constructor() { console.log('Instantiate Logger'); } } class DbManager { constructor({ logger }) { console.log('Instantiate DbManager with logger'); this.logger = logger; } } class UserService { constructor({ logger, dbManager }) { console.log('Instantiate UserService with logger and DbManager'); this.logger = logger; this.dbManager = dbManager; } } const logger = new Logger(); // Instantiate Logger const dbManager = new DbManager({ logger }); // Instantiate DbManager with logger const userService = new UserService({ logger, dbManager }); //Instantiate UserService with logger and DbManager |
On doit à chaque fois gérer les dépendances de chaque service. Voyons maintenant comment gérer automatiquement ces dépendances à l’aide d’un conteneur de dépendances qui utilise un proxy.
Voici la classe du conteneur :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | class Container { constructor() { // La liste des services non instanciés (ie : les classes des services) this.definitions = new Map(); // La liste des services instanciés this.services = new Map(); } register(name, cls) { // On ajoute une définition de service (ie : sa classe) et on l'identifie avec un no this.definitions.set(name, cls); } resolve(name) { // On vérifie que ce service est bien défini dans notre conteneur if (!this.definitions.has(name)) { throw new Error(`Unknown service ${name}`); } //Si le service a déjà été instancié on le renvoie if (this.services.has(name)) { return this.services.get(name); } // Sinon on récupère sa classe ... const definition = this.definitions.get(name); // ... et on l'instancie et la magie opère les dépendances sont chargées automatiquement ! const service = new definition(this.paramParser()); // On ajoute le service à la liste des services instanciés this.services.set(name, service); // Et on retourne le service return service; } // C'est ici que la magie opère ! paramParser() { return new Proxy( {}, { get: (target, name) => { // On retourne le service si celui-ci existe return this.resolve(name); } } ); } } // On créer notre conteneur const container = new Container(); // On enregistre nos services container.register('logger', Logger); container.register('dbManager', DbManager); container.register('userService', UserService); /* On récupère notre service userService ... * et magie notre container instancie tous les services que userService a besoin * et s'occupe automatiquement de l'injection : * Instantiate Logger * Instantiate DbManager with logger * Instantiate UserService with logger and DbManager */ const userService = container.resolve('userService'); |
Bon, il y a besoin d’une petite explication, tout se passe dans la méthode paramParser
celle-ci utilise un proxy qui permet de récupérer le nom des paramètres d’une fonction lorsque le destructuring de paramètre est utilisé. Pour chaque paramètre, elle vérifie, via la méthode resolve
, si un service portant ce nom existe, si c’est le cas elle le renvoie. Cela permet de créer de manière dynamique l’objet qui sera passé au constructeur de chaque service et ainsi faire de l’injection automatique.
Bien entendu pour que cela fonctionne vous devez impérativement utiliser le destructuring dans vos constructeurs :
1 | constructor({ logger, dbManager }){...} |
Vous pouvez vous amuser à améliorer cet exemple si vous le souhaitez, voici quelques pistes :
- Gérer l’enregistrement de fonction et de valeur (primitive ou objet), car dans l’exemple on ne gère que des classes;
- Ajouter une option lors de l’enregistrement d’un service pour savoir si celui-ci doit être un singleton ou non (pour le moment on ne gère que des singletons)
- Supprimer un service;
- etc.
la trappe set
La trappe set
est appelée lorsque l’on définit ou modifie une propriété d’un objet. La signature de la fonction est la suivante :
1 | get(target, property, value, receiver) |
Avec :
target
: L’objet que le proxy enveloppe;property
: Le nom de la propriété que l’on souhaite définir ou modifier;value
: La valeur que l’on souhaite affecter à la propriété;receiver
: Le proxy ou un objet qui en hérite.
Cette méthode doit renvoyer une valeur booléenne, true
si l’affectation a réussi, false
dans le cas contraire.
Cas d’utilisation
On va tout de suite passer à des cas d’utilisation de cette trappe pour mieux voir ce qu’il est possible de faire avec.
Créer un tableau d’entier
On commence par un exemple tout simple. On souhaite disposer d’un tableau qui ne contient que des entiers. Pour cela, il est possible de créer une nouvelle classe qui hérite de Array
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | class IntegerArray extends Array { constructor(...args) { super(...args); } push(...values) { for (let value of values) { if (!Number.isInteger(value)) { throw new TypeError('Array must only contains integers'); } } super.push(...values); } } const arr = new IntegerArray(); arr.push(14); arr[0] = 'toto'; // Cette opération est possible ! arr.push('test'); // TypeError: Array must only contains integers |
Malheureusement il est possible d’affecter n’importe quelle valeur via l’accesseur de propriété []
.
Voyons comment régler ce problème facilement à l’aide d’un proxy :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | const createIntegerArray = () => new Proxy([], { set(target, property, value, receiver) { // On vérifie si la valeur est un nombre entier if (!Number.isInteger(value)) { // Si ce n'est pas le cas on lève une exception throw new TypeError('Array must only contains integers'); } // On fait appel à la fonction Reflect.set comme nous l'avons vu pour la trappe get return Reflect.set(target, property, value, receiver); } }); const arr = createIntegerArray(); arr.push(1); arr.push(3); arr.push(5); console.log(arr); // [ 1, 3, 5 ] arr[0] = 'test'; // TypeError: Array must only contains integers arr.push('test'); // TypeError: Array must only contains integers |
On peut également reprendre notre classe IntegerArray
et utiliser le proxy comme ceci :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | class IntegerArray extends Array { constructor(...args) { super(...args); /* L'objet que doit encapsuler le proxy est l'objet que nous sommes en train de construire c'est-à-dire this */ return new Proxy(this, { set(target, property, value, receiver) { /* On vérifie si la valeur est un nombre entier */ if (!Number.isInteger(value)) { throw new TypeError('Array must only contains integers'); } /* On fait appel à la fonction Reflect.set comme nous l'avons vu pour la trappe get */ return Reflect.set(target, property, value, receiver); } }); } } const arr = new IntegerArray(); arr.push(1); arr.push(3); arr.push(5); console.log(arr); // [ 1, 3, 5 ] arr[0] = 'test'; // TypeError: Array must only contains integers arr.push('test'); // TypeError: Array must only contains integers |
Généralement, les constructeurs n’ont pas d’instruction return
. Ils sont chargés d’ajouter les différentes propriétés dans this
qui représente notre objet en construction. this
est ensuite renvoyé de manière implicite, mais il est possible de renvoyer un autre objet à la place comme ici où nous renvoyons notre proxy.
Valider des propriétés d’un objet
Voyons un exemple un peu plus compliqué que le précédent. Nous souhaitons valider les propriétés d’un objet lors d’une affectation ou d’une modification.
Prenons une classe User
:
1 2 3 4 5 6 7 8 9 | class User { constructor(username, email) { this.username = username; this.email = email; } } const user = new User('John', 'john@doe.com'); user.email = 'bad_format'; // Aucune erreur n'est levée |
Aucune vérification n’est faite, il est possible d’affecter n’importe quelle valeur.
Voyons comment régler ce souci à l’aide d’un proxy et des expressions régulières, car je sais que vous adorez ça :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | class User { constructor(username, email) { // On définit le format de données pour chacune des propriétés const schema = { username: /^[\w_-]{3,20}/, email: /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(.\w{2,3})+$/ }; // On créer une fonction qui va tester pour une propriété si une valeur respecte le format de donnée const validate = (property, value) => { if (!schema[property].test(value)) { throw new Error(`Invalid format for property "${property}"`); } return value; }; this.username = validate('username', username); this.email = validate('email', email); return new Proxy(this, { set(target, property, value, receiver) { // On vérifie que la propriété existe déjà if (property in target) { // On teste si la nouvelle valeur respecte le format value = validate(property, value); } // On affecte la nouvelle valeur grâce à la fonction Reflect.set return Reflect.set(target, property, value, receiver); } }); } } const user = new User('John', 'john@doe.com'); user.email = 'bad_format'; // Error: Invalid format for property "email" |
Vous pouvez également utiliser la librairie Joi pour définir des schémas de données plus complexes.
Observer des changements sur les propriétés d’un objet
On est parfois amené à devoir exécuter certaines opérations lorsqu’une propriété d’un objet est modifiée. L’utilisation d’un proxy permet très facilement d’observer les changements de valeur des propriétés d’un objet et d’exécuter des fonctions lorsque cela se produit :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | const observable = (obj, handlers) => { return new Proxy(obj, { set(target, property, value, receiver) { // On affecte la nouvelle valeur grâce à la fonction Reflect.set let success = Reflect.set(target, property, value, receiver); // On récupère les fonctions à exécuter qui concernent la propriété que l'on affecte const propertyHandlers = handlers[property]; /* On vérifie que l'affectation de la nouvelle valeur s'est bien déroulée et qu'il existe des fonctions concernant la propriété à exécuter */ if (success && propertyHandlers) { // On exécute chacune des fonctions propertyHandlers.forEach((handler) => handler(value)); } return success; } }); }; const obj = { value: 1 }; /* On créer pour chaque propriété que l'on souhaite observer un tableau de fonction à appeler lorsqu'un changement se produit */ const observableObj = observable(obj, { value: [ (value) => console.log(`Set ${value} to property a`), () => console.log('Process another operation') ] }); /* * Set 42 to property a * Process another operation */ observableObj.value = 42; |
Proxy révocable
Il est également possible de créer un objet Proxy
révocable :
1 | const { proxy, revoke } = Proxy.revocable(target, handler); |
Cette fonction retourne le proxy créé ainsi qu’une fonction revoke
qui permet de désactiver celui-ci.
Lorsque la méthode revoke
est appelée, toutes les trappes lèveront une exception TypeError
:
1 2 3 4 5 | const target = {}; const { proxy, revoke } = Proxy.revocable(target, {}); proxy.test = 'test1'; revoke(); proxy.test = 'test2'; // TypeError: Cannot perform 'set' on a proxy that has been revoked |
On peut par exemple donner accès à un objet de manière temporaire :
1 2 3 4 5 6 7 8 9 10 11 12 13 | const timeLimitedAccess = (obj, ttl) => { const { proxy, revoke } = Proxy.revocable(obj, {}); setTimeout(revoke, ttl); return proxy; }; const obj = { value: 1 }; const objProxy = timeLimitedAccess(obj, 1000); console.log(objProxy.value); // 1 setTimeout(() => console.log(objProxy.value), 2000); // TypeError: Cannot perform 'get' on a proxy that has been revoked |
Pour finir…
Nous avons vu comment utiliser l’objet Proxy en JavaScript avec seulement l’utilisation de deux trappes à savoir get
et set
dans des exemples relativement simples, j’aurais pu vous montrer encore pleins d’autres exemples comme un système de cache, rendre des propriétés privées (en attendant que ce soit pris en charge par le langage : https://github.com/tc39/proposal-class-fields) ou encore garder un historique des modifications sur un objet. Le nombre de trappes disponibles est au nombre de treize je vous laisse donc imaginer toutes les possibilités. Je ferai sûrement un second article sur l’utilisation des autres trappes si cela vous intéresse. N’hésitez pas à partager vos utilisations de l’objet Proxy
en commentaire !
Je suis lead developer dans une boîte spécialisée dans l’univers du streaming/gaming, et en parallèle, je m’éclate en tant que freelance. Passionné par l’écosystème JavaScript, je suis un inconditionnel de Node.js depuis 2011. J’adore échanger sur les nouvelles tendances et partager mon expérience avec les autres développeurs.
Si vous avez envie de papoter, n’hésitez pas à me retrouver sur Twitter, m’envoyer un petit email ou même laisser un commentaire.
Excellent article.
Petite coquille au début : jeBon
Merci ! c’est corrigé 😉