Présentation des proxys ES2015

Les proxys ES2015 (dans Chrome 49 et versions ultérieures) fournissent à JavaScript une API d'intercession, qui nous permet de piéger ou d'intercepter toutes les opérations sur un objet cible et de modifier son fonctionnement.

Les proxys ont de nombreuses utilisations, par exemple:

  • Interception
  • Virtualisation des objets
  • Gestion des ressources
  • Analyse ou journalisation pour le débogage
  • Sécurité et contrôle d'accès
  • Contrats d'utilisation des objets

L'API Proxy contient un constructeur Proxy qui reçoit un objet cible désigné et un objet de gestionnaire.

var target = { /* some properties */ };
var handler = { /* trap functions */ };
var proxy = new Proxy(target, handler);

Le comportement d'un proxy est contrôlé par le gestionnaire, qui peut modifier le comportement d'origine de l'objet cible de plusieurs manières utiles. Le gestionnaire contient des méthodes de trap facultatives (par exemple, .get(), .set() et .apply()) appelées lorsque l'opération correspondante est effectuée sur le proxy.

Interception

Commençons par prendre un objet simple et y ajouter un middleware d'interception à l'aide de l'API Proxy. N'oubliez pas que le premier paramètre transmis au constructeur est la cible (l'objet faisant l'objet d'un proxy) et le second est le gestionnaire (le proxy lui-même). C'est là que nous pouvons ajouter des hooks pour nos getters, setters ou tout autre comportement.

var target = {};

var superhero = new Proxy(target, {
    get: function(target, name, receiver) {
        console.log('get was called for:', name);
        return target[name];
    }
});

superhero.power = 'Flight';
console.log(superhero.power);

En exécutant le code ci-dessus dans Chrome 49, nous obtenons les résultats suivants:

get was called for: power  
"Flight"

Comme nous pouvons le constater dans la pratique, l'exécution de notre opération d'obtention ou de définition de propriété sur l'objet proxy a correctement entraîné un appel au niveau méta de l'intercepteur correspondant sur le gestionnaire. Les opérations de gestionnaire incluent les lectures de propriétés, l'attribution de propriétés et l'application de fonctions, qui sont toutes transmises au trap correspondant.

La fonction de trappe peut, si elle le souhaite, implémenter une opération de manière arbitraire (par exemple, en transmettant l'opération à l'objet cible). C'est bien ce qui se passe par défaut si aucun piège n'est spécifié. Par exemple, voici un proxy de transfert sans opération qui fait exactement cela:

var target = {};

var proxy = new Proxy(target, {});
    // operation forwarded to the target
proxy.paul = 'irish';
// 'irish'. The operation has been  forwarded
console.log(target.paul);

Nous venons de voir comment mettre en place un proxy pour des objets simples, mais nous pouvons tout aussi facilement mettre en place un proxy pour un objet de fonction, où une fonction est notre cible. Cette fois, nous allons utiliser le piège handler.apply():

// Proxying a function object
function sum(a, b) {
    return a + b;
}

var handler = {
    apply: function(target, thisArg, argumentsList) {
        console.log(`Calculate sum: ${argumentsList}`);
        return target.apply(thisArg, argumentsList);
    }
};

var proxy = new Proxy(sum, handler);
proxy(1, 2);
// Calculate sum: 1, 2
// 3

Identifier les proxys

L'identité d'un proxy peut être observée à l'aide des opérateurs d'égalité JavaScript (== et ===). Comme nous le savons, lorsqu'ils sont appliqués à deux objets, ces opérateurs comparent les identités des objets. L'exemple suivant illustre ce comportement. La comparaison de deux proxys distincts renvoie la valeur "false", même si les cibles sous-jacentes sont identiques. De même, l'objet cible est différent de tous ses proxys:

// Continuing previous example

var proxy2 = new Proxy (sum, handler);
(proxy==proxy2); // false
(proxy==sum); // false

Idéalement, vous ne devriez pas pouvoir distinguer un proxy d'un objet non proxy, de sorte que la mise en place d'un proxy n'affecte pas vraiment le résultat de votre application. C'est l'une des raisons pour lesquelles l'API Proxy n'inclut pas de moyen de vérifier si un objet est un proxy ni ne fournit de pièges pour toutes les opérations sur les objets.

Cas d'utilisation

Comme indiqué, les proxys ont un large éventail de cas d'utilisation. Beaucoup de ces éléments, comme le contrôle des accès et le profilage, relèvent des enveloppes génériques: des proxys qui encapsulent d'autres objets dans le même "espace" d'adresse. La virtualisation a également été mentionnée. Les objets virtuels sont des proxys qui émulent d'autres objets sans que ces objets aient besoin d'être dans le même espace d'adressage. Par exemple, les objets distants (qui émulent des objets dans d'autres espaces) et les futures transparents (qui émulent des résultats qui ne sont pas encore calculés).

Proxys en tant que gestionnaires

Un cas d'utilisation assez courant des gestionnaires de proxy consiste à effectuer des vérifications de validation ou de contrôle des accès avant d'effectuer une opération sur un objet encapsulé. L'opération n'est transmise que si la vérification réussit. L'exemple de validation ci-dessous illustre ce point:

var validator = {
    set: function(obj, prop, value) {
    if (prop === 'yearOfBirth') {
        if (!Number.isInteger(value)) {
        throw new TypeError('The yearOfBirth is not an integer');
        }

        if (value > 3000) {
        throw new RangeError('The yearOfBirth seems invalid');
        }
    }

    // The default behavior to store the value
    obj[prop] = value;
    }
};

var person = new Proxy({}, validator);

person.yearOfBirth = 1986;
console.log(person.yearOfBirth); // 1986
person.yearOfBirth = 'eighties'; // Throws an exception
person.yearOfBirth = 3030; // Throws an exception

Des exemples plus complexes de ce modèle peuvent prendre en compte toutes les différentes opérations que les gestionnaires de proxy peuvent intercepter. On peut imaginer qu'une implémentation doit dupliquer le modèle de vérification des accès et de transfert de l'opération dans chaque trap.

Il peut être difficile d'extraire facilement cette information, car chaque opération peut devoir être transférée différemment. Dans un scénario idéal, si toutes les opérations pouvaient être acheminées de manière uniforme via un seul piège, le gestionnaire n'aurait besoin d'effectuer la vérification de validation qu'une seule fois dans le piège. Pour ce faire, implémentez le gestionnaire de proxy lui-même en tant que proxy. Malheureusement, cela dépasse le cadre de cet article.

Extension d'objet

Un autre cas d'utilisation courant des proxys consiste à étendre ou à redéfinir la sémantique des opérations sur les objets. Vous pouvez par exemple souhaiter qu'un gestionnaire consigne les opérations, informe les observateurs, génère des exceptions au lieu de renvoyer "undefined" ou redirige les opérations vers différentes cibles à des fins de stockage. Dans ce cas, l'utilisation d'un proxy peut entraîner un résultat très différent de celui obtenu avec l'objet cible.

function extend(sup,base) {

    var descriptor = Object.getOwnPropertyDescriptor(base.prototype,"constructor");

    base.prototype = Object.create(sup.prototype);

    var handler = {
    construct: function(target, args) {
        var obj = Object.create(base.prototype);
        this.apply(target,obj, args);
        return obj;
    },

    apply: function(target, that, args) {
        sup.apply(that,args);
        base.apply(that,args);
    }
    };

    var proxy = new Proxy(base, handler);
    descriptor.value = proxy;
    Object.defineProperty(base.prototype, "constructor", descriptor);
    return proxy;
}

var Vehicle = function(name){
    this.name = name;
};

var Car = extend(Vehicle, function(name, year) {
    this.year = year;
});

Car.prototype.style = "Saloon";

var Tesla = new Car("Model S", 2016);

console.log(Tesla.style); // "Saloon"
console.log(Tesla.name); // "Model S"
console.log(Tesla.year);  // 2016

Contrôle des accès

Le contrôle des accès est un autre bon cas d'utilisation des proxys. Plutôt que de transmettre un objet cible à un code non approuvé, vous pouvez transmettre son proxy enveloppé dans une sorte de membrane protectrice. Une fois que l'application estime que le code non approuvé a terminé une tâche particulière, elle peut révoquer la référence qui détache le proxy de sa cible. La membrane étendrait ce détachement de manière récursive à tous les objets accessibles à partir de la cible d'origine définie.

Utiliser la réflexion avec des proxys

Reflect est un nouvel objet intégré qui fournit des méthodes pour les opérations JavaScript interceptables, très utiles pour travailler avec des proxys. En fait, les méthodes Reflect sont les mêmes que celles des gestionnaires de proxy.

Les langages à typage statique tels que Python ou C# proposent depuis longtemps une API de réflexion, mais JavaScript n'en a pas vraiment besoin, car il s'agit d'un langage dynamique. On peut affirmer qu'ES5 dispose déjà de nombreuses fonctionnalités de réflexion, telles que Array.isArray() ou Object.getOwnPropertyDescriptor(), qui seraient considérées comme des réflexions dans d'autres langages. ES2015 introduit une API de réflexion qui hébergera les futures méthodes de cette catégorie, ce qui les rendra plus faciles à comprendre. Cela est logique, car Object est censé être un prototype de base plutôt qu'un conteneur pour les méthodes de réflexion.

À l'aide de Reflect, nous pouvons améliorer notre exemple de super-héros précédent pour une interception de champ appropriée sur nos pièges get et set comme suit:

// Field interception with Proxy and the Reflect API

var pioneer = new Proxy({}, {
    get: function(target, name, receiver) {
        console.log(`get called for field: ${name}`);
        return Reflect.get(target, name, receiver);
    },

    set: function(target, name, value, receiver) {
        console.log(`set called for field: ${name} and value: ${value}`);
        return Reflect.set(target, name, value, receiver);
    }
});

pioneer.firstName = 'Grace';
pioneer.secondName = 'Hopper';
// Grace
pioneer.firstName

Le résultat est le suivant:

set called for field: firstName and value: Grace
set called for field: secondName and value: Hopper
get called for field: firstName

Voici un autre exemple:

  • Encapsulez une définition de proxy dans un constructeur personnalisé pour éviter de créer manuellement un proxy chaque fois que vous souhaitez utiliser une logique spécifique.

  • Ajoutez la possibilité d'enregistrer les modifications, mais uniquement si les données ont effectivement été modifiées (hypothétiquement, car l'opération d'enregistrement est très coûteuse).

function Customer() {

    var proxy = new Proxy({
    save: function(){
        if (!this.dirty){
        return console.log('Not saving, object still clean');
        }
        console.log('Trying an expensive saving operation: ', this.changedProperties);
    },

    }, {

    set: function(target, name, value, receiver) {
        target.dirty = true;
        target.changedProperties = target.changedProperties || [];

        if(target.changedProperties.indexOf(name) == -1){
        target.changedProperties.push(name);
        }
        return Reflect.set(target, name, value, receiver);
    }

    });

    return proxy;
}


var customer = new Customer();

customer.name = 'seth';
customer.surname = 'thompson';
// Trying an expensive saving operation:  ["name", "surname"]
customer.save();

Pour plus d'exemples d'API Reflect, consultez Proxys ES6 par Tagtree.

Polyfilling Object.observe()

Bien que nous disions au revoir à Object.observe(), il est désormais possible de les polyfiller à l'aide de proxys ES2015. Simon Blackwell a récemment écrit un shim Object.observe() basé sur un proxy qui vaut la peine d'être consulté. Erik Arvidsson a également écrit une version assez complète des spécifications en 2012.

Prise en charge des navigateurs

Les proxys ES2015 sont compatibles avec Chrome 49, Opera, Microsoft Edge et Firefox. Les signaux publics concernant cette fonctionnalité dans Safari sont mitigés, mais nous restons optimistes. Reflect est disponible dans Chrome, Opera et Firefox, et est en cours de développement pour Microsoft Edge.

Google a publié un polyfill limité pour Proxy. Cette option ne peut être utilisée que pour les enveloppes génériques, car elle ne peut proxyfier que les propriétés connues au moment de la création d'un proxy.

Documentation complémentaire