11 juillet 2023

Proxy et Reflect

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. get pour lire une propriĂ©tĂ© de target, set pour Ă©crire une propriĂ©tĂ© dans target, 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.

  1. Une opĂ©ration d’écriture proxy.test = dĂ©finit la valeur sur target.
  2. Une opération de lecture proxy.test renvoie la valeur de target.
  3. L’itĂ©ration sur le proxy renvoie les valeurs de target.

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
Invariants

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 retourner true si la valeur a Ă©tĂ© Ă©crite avec succĂšs, sinon false.
  • [[Delete]] doit retourner true si la valeur a Ă©tĂ© supprimĂ©e avec succĂšs, sinon false.
  • 
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 au new proxy,
  • property – nom de la propriĂ©tĂ©,
  • receiver – si la propriĂ©tĂ© cible est un getter, le receiver est l’objet qui sera utilisĂ© comme this dans son appel. Habituellement, c’est l’objet proxy lui-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 :

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 au new proxy,
  • property – nom de la propriĂ©tĂ©,
  • value – valeur de la propriĂ©tĂ©,
  • receiver – similaire au piĂšge get, 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.

N’oubliez pas de retouner true

Comme 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’indicateur enumerable (les indicateurs de propriĂ©tĂ© ont Ă©tĂ© expliquĂ©s dans l’article Attributs et descripteurs de propriĂ©tĂ©s).
  • for..in boucle sur les clĂ©s non symboliques avec le drapeau enumerable, 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:

  • get lancer une erreur lors de la lecture d’une telle propriĂ©tĂ©,
  • set lancer une erreur lors de l’écriture,
  • deleteProperty lancer une erreur lors de la suppression,
  • ownKeys pour exclure les propriĂ©tĂ©s commençant par _ de for..in et les mĂ©thodes comme Object.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.

PropriĂ©tĂ©s privĂ©es d’une classe

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:

  • target est l’objet cible (la fonction est un objet en JavaScript),
  • thisArg est la valeur de this.
  • args est 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.get lit une propriĂ©tĂ© d’objet.
  • Reflect.set Ă©crit une propriĂ©tĂ© d’objet et renvoie true en cas de succĂšs, false dans 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 (*).

  1. Lorsque nous lisons admin.name, comme l’objet admin n’a pas une telle propriĂ©tĂ©, la recherche va Ă  son prototype.

  2. Le prototype est userProxy.

  3. Lors de la lecture de la propriĂ©tĂ© name du proxy, son piĂšge get se dĂ©clenche et la renvoie Ă  partir de l’objet d’origine en tant que target[prop] dans la ligne (*).

    Un appel Ă  target[prop], lorsque prop est un getter, exĂ©cute son code dans le contexte this=target. Le rĂ©sultat est donc this._name de l’objet target d’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 internes

Une 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.

Important :

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Ăšge construct).
  • 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 this pour 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”.

Exercices

Habituellement, une tentative de lecture d’une propriĂ©tĂ© inexistante renvoie undefined.

CrĂ©ez Ă  la place un proxy qui gĂ©nĂšre une erreur pour une tentative de lecture d’une propriĂ©tĂ© inexistante.

Cela peut aider à détecter précocement les erreurs de programmation.

Écrivez une fonction wrap(target) qui prend un objet target et retourne un proxy qui ajoute cet aspect fonctionnel.

VoilĂ  comment cela devrait fonctionner:

let user = {
  name: "John"
};

function wrap(target) {
  return new Proxy(target, {
      /* your code */
  });
}

user = wrap(user);

alert(user.name); // John
alert(user.age); // ReferenceError: la propriété n'existe pas : "age"
let user = {
  name: "John"
};

function wrap(target) {
  return new Proxy(target, {
    get(target, prop, receiver) {
      if (prop in target) {
        return Reflect.get(target, prop, receiver);
      } else {
        throw new ReferenceError(`Property doesn't exist: "${prop}"`)
      }
    }
  });
}

user = wrap(user);

alert(user.name); // John
alert(user.age); // ReferenceError: Property doesn't exist: "age"

Dans certains langages de programmation, nous pouvons accĂ©der aux Ă©lĂ©ments du tableau Ă  l’aide d’index nĂ©gatifs, comptĂ©s Ă  partir de la fin.

comme ça:

let array = [1, 2, 3];

array[-1]; // 3, le premier élément en partant de la fin
array[-2]; // 2, le second élément en partant de la fin
array[-3]; // 1, le troisiÚme élément en partant de la fin

En d’autres termes, array[-N] est identique à array[array.length - N].

Créez un proxy pour implémenter ce comportement.

VoilĂ  comment cela devrait fonctionner:

let array = [1, 2, 3];

array = new Proxy(array, {
  /* your code */
});

alert( array[-1] ); // 3
alert( array[-2] ); // 2

// Les autres fonctionnalitĂ©s de array doivent ĂȘtre conservĂ©es
let array = [1, 2, 3];

array = new Proxy(array, {
  get(target, prop, receiver) {
    if (prop < 0) {
      // mĂȘme si on y accĂšde comme arr[1]
      // prop est une chaĂźne, il faut donc la convertir en nombre
      prop = +prop + target.length;
    }
    return Reflect.get(target, prop, receiver);
  }
});


alert(array[-1]); // 3
alert(array[-2]); // 2

CrĂ©ez une fonction makeObservable(target) qui “rend l’objet observable” en renvoyant un proxy.

Voici comment cela devrait fonctionner:

function makeObservable(target) {
  /* your code */
}

let user = {};
user = makeObservable(user);

user.observe((key, value) => {
  alert(`SET ${key}=${value}`);
});

user.name = "John"; // alerts: SET name=John

En d’autres termes, un objet retournĂ© par makeObservable est exactement comme celui d’origine, mais possĂšde Ă©galement la mĂ©thode observe(handler) qui dĂ©finit la fonction de handler Ă  appeler lors de tout changement de propriĂ©tĂ©.

Chaque fois qu’une propriĂ©tĂ© change, le handler(key, value) est appelĂ© avec le nom et la valeur de la propriĂ©tĂ©.

P.S. Dans cette tĂąche, veillez uniquement Ă  Ă©crire sur une propriĂ©tĂ©. D’autres opĂ©rations peuvent ĂȘtre implĂ©mentĂ©es de maniĂšre similaire.

La solution se compose de deux parties:

  1. Chaque fois que .observe(handler) est appelĂ©, nous devons nous souvenir du handler quelque part, pour pouvoir l’appeler plus tard. Nous pouvons stocker des handler directement dans l’objet, en utilisant notre symbole comme clĂ© de propriĂ©tĂ©
  2. Nous avons besoin d’un proxy avec le piùge set pour appeler les handler en cas de changement
let handlers = Symbol('handlers');

function makeObservable(target) {
  // 1. initialiser le stockage de l'handler
  target[handlers] = [];

  // Stocker la fonction de l'handler dans un tableau pour les appels futurs
  target.observe = function(handler) {
    this[handlers].push(handler);
  };

  // 2. Créer un proxy pour gérer les modifications
  return new Proxy(target, {
    set(target, property, value, receiver) {
      let success = Reflect.set(...arguments); // transmettre l'opération à l'objet
      if (success) { // s'il n'y a pas eu d'erreur lors de la définition de la propriété
        // appeler tous les handler
        target[handlers].forEach(handler => handler(property, value));
      }
      return success;
    }
  });
}

let user = {};

user = makeObservable(user);

user.observe((key, value) => {
  alert(`SET ${key}=${value}`);
});

user.name = "John";
Carte du tutoriel

Commentaires

lire ceci avant de commenter

  • Si vous avez des amĂ©liorations Ă  suggĂ©rer, merci de soumettre une issue GitHub ou une pull request au lieu de commenter.
  • Si vous ne comprenez pas quelque chose dans l'article, merci de prĂ©ciser.
  • Pour insĂ©rer quelques bouts de code, utilisez la balise <code>, pour plusieurs lignes – enveloppez-les avec la balise <pre>, pour plus de 10 lignes - utilisez une sandbox (plnkr, jsbin, codepen
)