Proxy
=====

Un objet **Proxy** permet de créer un intermédiaire pour un autre objet et qui peut intercepter et redéfinir certaines opérations fondamentales pour lui.

Description
-----------

Un objet `Proxy` permet de créer un objet qui peut être utilisé à la place de l'objet original en redéfinissant certaines opérations fondamentales comme l'accès, la modification et la définition de propriétés. Les objets `Proxy` sont généralement utilisés pour journaliser l'accès aux propriétés, valider, formater ou nettoyer des valeurs saisies, etc.

La création d'un objet `Proxy` se fait avec deux paramètres :

`cible`

L'objet original devant lequel on veut placer un intermédiaire

`gestionnaire`

Un objet qui définit les opérations qui seront interceptées et comment celles-ci seront redéfinies.

Dans l'exemple qui suit, on a une cible simple avec deux propriétés et un gestionnaire encore plus simple, sans propriété.


```js
const cible = {
  message1: "coucou",
  message2: "tout le monde",
};

const gestionnaire1 = {};

const proxy1 = new Proxy(cible, gestionnaire1);
```


Le gestionnaire étant vide, le proxy se comporte à l'identique de la cible :


```js
console.log(proxy1.message1); // coucou
console.log(proxy1.message2); // tout le monde
```


Pour adapter le proxy, on définit des fonctions sur le gestionnaire :


```js
const cible = {
  message1: "coucou",
  message2: "tout le monde",
};

const gestionnaire2 = {
  get(cible, prop, recepteur) {
    return "le monde";
  },
};

const proxy2 = new Proxy(cible, gestionnaire2);
```


Ici, on a fourni une implémentation du gestionnaire `get()`, qui intercepte les tentatives d'accès aux propriétés de la cible.

Les fonctions d'un gestionnaire sont parfois appelées des *trappes*, car les appels originaux tombent dans ces trappes. Celle qui est utilisée dans `gestionnaire2` redéfinit l'accès pour toutes les propriétés :


```js
console.log(proxy2.message1); // le monde
console.log(proxy2.message2); // le monde
```


Avec `Reflect`, on peut rediriger certains accesseurs vers leur comportement original et en redéfinir d'autres :


```js
const cible = {
  message1: "coucou",
  message2: "tout le monde",
};

const gestionnaire3 = {
  get(cible, prop, recepteur) {
    if (prop === "message2") {
      return "le monde";
    }
    return Reflect.get(...arguments);
  },
};

const proxy3 = new Proxy(cible, gestionnaire3);

console.log(proxy3.message1); // coucou
console.log(proxy3.message2); // le monde
```

Constructeur
------------

`Proxy()`

Crée un nouvel objet `Proxy`.

Méthodes statiques
------------------

`Proxy.revocable()`

Crée un objet `Proxy` révocable.

Exemples
--------

### Exemple simple

Dans ce court exemple, on renvoie le nombre `37` comme valeur par défaut lorsque la propriété nommée n'est pas présente dans l'objet. Pour cela, on utilise le gestionnaire correspondant à `get()` (en-US).


```js
const handler = {
  get(obj, prop) {
    return prop in obj ? obj[prop] : 37;
  },
};

const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b);
// 1, undefined

console.log("c" in p, p.c);
// false, 37
```

### Proxy « invisible »

Dans cet exemple, le proxy transfère toutes les opérations qui sont appliquées à l'objet cible.


```js
const target = {};
const p = new Proxy(target, {});

p.a = 37;
// L'opération est transmise à la cible par le proxy

console.log(target.a);
// 37
// L'opération a bien été transmise
```


On notera que bien que ceci fonctionne pour les objets JavaScript construits dans les scripts, ça ne fonctionne pas pour les objets natifs de l'environnement (comme les éléments du DOM dans un navigateur).

### Validation

En utilisant un `Proxy`, on peut simplement valider les valeurs passées à un objet. Dans cet exemple, on utilise le gestionnaire correspondant à `set()` (en-US).


```js
let validateur = {
  set: function (obj, prop, valeur) {
    if (prop === "age") {
      if (!Number.isInteger(valeur)) {
        throw new TypeError("Cet a n'est pas un entier.");
      }
      if (valeur > 200) {
        throw new RangeError("Cet âge semble invalide.");
      }
    }

    // Le comportement par défaut : enregistrer la valeur
    obj[prop] = valeur;

    // On indique le succès de l'opération
    return true;
  },
};

const personne = new Proxy({}, validateur);

personne.age = 100;
console.log(personne.age); // 100
personne.age = "jeune"; // lève une exception
personne.age = 300; // lève une exception
```

### Étendre un constructeur

En utilisant une fonction proxy, on peut étendre un constructeur avec un nouveau constructeur. Dans cet exemple, on utilise les gestionnaires correspondants à `construct()` (en-US) et `apply()` (en-US).


```js
function etendre(sup, base) {
  var descripteur = Object.getOwnPropertyDescriptor(
    base.prototype,
    "constructor",
  );
  base.prototype = Object.create(sup.prototype);
  var gestionnaire = {
    construct: function (cible, args) {
      var obj = Object.create(base.prototype);
      this.apply(cible, obj, args);
      return obj;
    },
    apply: function (cible, that, args) {
      sup.apply(that, args);
      base.apply(that, args);
    },
  };
  var proxy = new Proxy(base, gestionnaire);
  descripteur.value = proxy;
  Object.defineProperty(base.prototype, "constructor", descripteur);
  return proxy;
}

var Personne = function (nom) {
  this.nom = nom;
};

var Garcon = etendre(Personne, function (nom, âge) {
  this.âge = âge;
});

Garcon.prototype.genre = "M";

var Pierre = new Garcon("Pierre", 13);
console.log(Pierre.genre); // "M"
console.log(Pierre.nom); // "Pierre"
console.log(Pierre.âge); // 13
```

### Manipuler les nœuds DOM

Dans cet exemple, on utilise `Proxy` afin qu'un attribut alterne entre deux éléments différents : si on définit l'attribut sur un élément, il sera retiré de l'autre.

On crée un objet `vue` qui est un proxy pour l'objet avec une `selected`. Le gestionnaire du proxy définit la fonction `set()`.

Lorsqu'on affecte un élément HTML à `vue.selected`, l'attribut `'aria-selected'` de l'élément est placé à `true`. Si on affecte ensuite un autre élément à `vue.selected`, ce nouvel élément aura l'attribut `'aria-selected'` défini à `true` et l'élément précédent verra son attribut `'aria-selected'` automatiquement défini à `false`.


```js
let vue = new Proxy(
  {
    selected: null,
  },
  {
    set(obj, prop, nouvelleValeur) {
      let ancienneValeur = obj[prop];

      if (prop === "selected") {
        if (ancienneValeur) {
          ancienneValeur.setAttribute("aria-selected", "false");
        }
        if (nouvelleValeur) {
          nouvelleValeur.setAttribute("aria-selected", "true");
        }
      }

      // Le comportement par défaut : enregistrer la valeur
      obj[prop] = nouvelleValeur;

      // On indique le succès de l'opération
      return true;
    },
  },
);

const element1 = document.getElementById("elem-1");
const element2 = document.getElementById("elem-2");

// on sélectionne element1
vue.selected = element1;

console.log(`element1 : ${element1.getAttribute("aria-selected")}`);
// element1 : true

// on sélectionne element2 et cela entraîne
// la déselection automatique de element1
vue.selected = element2;

console.log(`element1 : ${element1.getAttribute("aria-selected")}`);
// element1 : false

console.log(`element2 : ${element2.getAttribute("aria-selected")}`);
// element2 : true
```

### Corriger une valeur et ajouter une propriété supplémentaire

Dans l'exemple qui suit, le proxy `produits` évalue la valeur passée et la convertit en tableau si besoin. L'objet prend également en charge la propriété supplémentaire `dernierNavigateur` à la fois comme accesseur et mutateur.


```js
let produits = new Proxy(
  {
    navigateurs: ["Internet Explorer", "Netscape"],
  },
  {
    get(obj, prop) {
      // Une propriété supplémentaire
      if (prop === "dernierNavigateur") {
        return obj.navigateurs[obj.navigateurs.length - 1];
      }

      // Le comportement par défaut : renvoyer la valeur
      return obj[prop];
    },
    set(obj, prop, valeur) {
      // Une propriété supplémentaire
      if (prop === "dernierNavigateur") {
        obj.navigateurs.push(valeur);
        return true;
      }

      // on convertit la valeur si ce n'est pas un tableau
      if (typeof valeur === "string") {
        valeur = [valeur];
      }

      // Le comportement par défaut : enregistrer la valeur
      obj[prop] = valeur;

      // On indique le succès de l'opération
      return true;
    },
  },
);

console.log(produits.navigateurs);
// ['Internet Explorer', 'Netscape']

produits.navigateurs = "Firefox";
// on passe une chaîne
console.log(produits.navigateurs);
// ['Firefox'] <- pas de problème, elle est convertie en tableau

produits.dernierNavigateur = "Chrome";

console.log(produits.navigateurs);
// ['Firefox', 'Chrome']

console.log(produits.dernierNavigateur);
// 'Chrome'
```

### Trouver un élément dans un tableau grâce à sa propriété

Dans cet exemple, ce proxy étend le tableau avec des fonctionnalités supplémentaires. Ici, on définit des propriétés sans utiliser `Object.defineProperties()`. Cet exemple pourrait être adapté pour trouver la ligne d'un tableau à partir d'une de ces cellules (la cible serait alors `table.rows` (en-US)).


```js
let produits = new Proxy(
  [
    { nom: "Firefox", type: "navigateur" },
    { nom: "SeaMonkey", type: "navigateur" },
    { nom: "Thunderbird", type: "client mail" },
  ],
  {
    get(obj, prop) {
      // Le comportement par défaut : on renvoie la valeur
      // prop est généralement un entier
      if (prop in obj) {
        return obj[prop];
      }

      // On obtient le nombre de produits
      // un alias pour products.length
      if (prop === "nombre") {
        return obj.length;
      }

      let resultat,
        types = {};

      for (let produit of obj) {
        if (produit.nom === prop) {
          resultat = produit;
        }
        if (types[produit.type]) {
          types[produit.type].push(produit);
        } else {
          types[produit.type] = [produit];
        }
      }

      // Obtenir un produit grâce à un nom
      if (resultat) {
        return resultat;
      }

      // Obtenir un produit par type
      if (prop in types) {
        return types[prop];
      }

      // Obtenir les types de produits
      if (prop === "types") {
        return Object.keys(types);
      }

      return undefined;
    },
  },
);

console.log(produits[0]);
// { nom: 'Firefox', type: 'navigateur' }

console.log(produits["Firefox"]);
// { nom: 'Firefox', type: 'navigateur' }

console.log(produits["Chrome"]);
// undefined

console.log(produits.navigateur);
// [{ nom: 'Firefox', type: 'navigateur' }, { nom: 'SeaMonkey', type: 'navigateur' }]

console.log(produits.types);
// ['navigateur', 'client mail']

console.log(produits.nombre);
// 3
```

### Un exemple avec toutes les trappes

Pour illustrer l'ensemble des trappes, on tente de « proxifier » un objet non natif : l'objet global `docCookies` créé grâce à cet exemple.


```js
/\*
 var docCookies = ... définir l'objet "docCookies" grâce à
 https://reference.codeproject.com/dom/document/cookie/simple\_document.cookie\_framework
\*/

var docCookies = new Proxy(docCookies, {
  get(oTarget, sKey) {
    return oTarget[sKey] || oTarget.getItem(sKey) || undefined;
  },
  set: function (oTarget, sKey, vValue) {
    if (sKey in oTarget) {
      return false;
    }
    return oTarget.setItem(sKey, vValue);
  },
  deleteProperty: function (oTarget, sKey) {
    if ((!sKey) in oTarget) {
      return false;
    }
    return oTarget.removeItem(sKey);
  },
  ownKeys: function (oTarget, sKey) {
    return oTarget.keys();
  },
  has: function (oTarget, sKey) {
    return sKey in oTarget || oTarget.hasItem(sKey);
  },
  defineProperty: function (oTarget, sKey, oDesc) {
    if (oDesc && "value" in oDesc) {
      oTarget.setItem(sKey, oDesc.value);
    }
    return oTarget;
  },
  getOwnPropertyDescriptor: function (oTarget, sKey) {
    var vValue = oTarget.getItem(sKey);
    return vValue
      ? {
          value: vValue,
          writable: true,
          enumerable: true,
          configurable: false,
        }
      : undefined;
  },
});

/\* Test \*/

console.log((docCookies.monCookie1 = "Première valeur"));
console.log(docCookies.getItem("monCookie1"));

docCookies.setItem("monCookie1", "Valeur modifiée");
console.log(docCookies.monCookie1);
```

Spécifications
--------------

| Specification |
| --- |
| ECMAScript Language Specification # sec-proxy-objects |

Compatibilité des navigateurs
-----------------------------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

Voir aussi
----------

* La vidéo de la présentation « Les proxies c'est génial » de BrendanEich, à JSConf (le support de la présentation)
* Un tutoriel sur les proxies (en anglais)