Présentation des proxys ES2015

Addy Osmani
Addy Osmani

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

Les proxies ont un grand nombre d'utilisations, y compris:

  • Interception
  • Virtualisation des objets
  • Gestion des ressources
  • Profilage 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 de proxy qui accepte 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 target de plusieurs manières utiles. Le gestionnaire contient des méthodes de trap facultatives (par exemple, .get(), .set(), .apply()) appelées lorsque l'opération correspondante est effectuée sur le proxy.

Interception

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

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 ce qui suit:

get was called for: power  
"Flight"

Comme nous pouvons le constater en pratique, l'exécution de notre propriété get ou de notre propriété définie sur l'objet proxy entraînait correctement un appel de méta-niveau vers le piège 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 la fonction, toutes transmises au piège correspondant.

La fonction trap peut, si elle le souhaite, implémenter une opération de manière arbitraire (par exemple, transfert de l'opération à l'objet cible). C'est en effet ce qui se produit par défaut si aucun piège n'est spécifié. Par exemple, voici un proxy de transfert no-op qui effectue exactement cette opération:

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 d'examiner la transmission d'objets ordinaires par proxy, mais nous pouvons tout aussi facilement transmettre un objet fonction au proxy, 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 d'objet. 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 l'un de ses proxys:

// Continuing previous example

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

Idéalement, vous ne devriez pas être en mesure de 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 une raison pour laquelle l'API Proxy n'inclut pas de moyen de vérifier si un objet est un proxy et qu'il fournit des interceptions pour toutes les opérations sur les objets.

Cas d'utilisation

Comme indiqué précédemment, les proxys ont un large éventail de cas d'utilisation. Un grand nombre de ces éléments, tels que le contrôle des accès et le profilage, relèvent des wrappers génériques, c'est-à-dire 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 ceux-ci n'aient besoin de se trouver dans le même espace d'adressage. Les objets distants (qui émulent des objets dans d'autres espaces) et les objets Future transparents (émulation de résultats qui ne sont pas encore calculés) en sont des exemples.

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 d'accès avant d'effectuer une opération sur un objet encapsulé. L'opération n'est transférée que si la vérification réussit. L'exemple de validation ci-dessous le démontre:

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 pourraient prendre en compte toutes les différentes opérations que les gestionnaires de proxy peuvent intercepter. On pourrait imaginer une implémentation qui devra dupliquer le schéma de vérification des accès et de transfert de l'opération dans chaque trap.

Il peut être difficile d'extraire facilement, étant donné que chaque opération doit être transmise différemment. Dans un scénario parfait, si toutes les opérations pouvaient être acheminées uniformément via un seul piège, le gestionnaire n'aurait besoin d'effectuer le contrôle de validation qu'une seule fois dans ce seul piège. Pour ce faire, vous pouvez implémenter lui-même le gestionnaire de proxy en tant que proxy. Malheureusement, ce point n'est pas abordé dans 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. Par exemple, vous pouvez souhaiter qu'un gestionnaire enregistre les opérations, notifie les observateurs, génère des exceptions au lieu de renvoyer des opérations non définies ou redirige les opérations vers différentes cibles à des fins de stockage. Dans ces cas, l'utilisation d'un proxy peut conduire à un résultat très différent de l'utilisation de 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 d’accès est un autre bon cas d’utilisation des proxies. Plutôt que de transmettre un objet cible à un morceau de code non fiable, on peut transmettre son proxy encapsulé dans une sorte de membrane de protection. Lorsque l'application estime que le code non approuvé a terminé une tâche particulière, elle peut révoquer la référence, ce qui dissocie le proxy de sa cible. La membrane étendrait ce détachement de manière récursive à tous les objets accessibles depuis la cible initiale définie.

Utiliser la réflexion avec des proxies

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

Les langages de type statique tels que Python ou C# proposent depuis longtemps une API de réflexion, mais JavaScript n'a pas vraiment besoin d'être un langage dynamique. On peut dire qu'ES5 dispose déjà de pas mal de caractéristiques de réflexion, comme Array.isArray() ou Object.getOwnPropertyDescriptor(), qui seraient considérées comme de la réflexion dans d'autres langues. ES2015 introduit une API Reflection qui intégrera les futures méthodes pour cette catégorie, ce qui facilitera leur analyse. Cela est logique, car l'objet est destiné à être un prototype de base plutôt qu'un bucket pour les méthodes de réflexion.

En utilisant Reflect, nous pouvons améliorer notre précédent exemple de super-héros, qui permet d'intercepter correctement les champs "get" et "set trap" 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

Ce qui donne:

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

Autre exemple:

  • Encapsulez une définition de proxy dans un constructeur personnalisé pour éviter de créer manuellement un nouveau proxy chaque fois que nous voulons travailler avec une logique spécifique.

  • Ajoutez la possibilité d'"enregistrer" les modifications, mais uniquement si les données ont été réellement modifiées (en raison du coût élevé de l'opération d'enregistrement).

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 la section Proxies ES6 de Tagtree.

Polyfilling Object.observe()

Bien que nous disons au revoir à Object.observe(), il est désormais possible de les émuler à l'aide de proxys ES2015. Simon Blackwell a récemment écrit un shim Object.observe() basé sur Proxy, qui mérite d'être consulté. Erik Arvidsson a également écrit une version assez complète depuis 2012.

Prise en charge des navigateurs

Les proxys ES2015 sont compatibles avec Chrome 49, Opera, Microsoft Edge et Firefox. Les signaux publics pour 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. Il ne peut être utilisé que pour les wrappers génériques, car il ne peut servir de proxy que pour les propriétés connues au moment de la création d'un proxy.

Complément d'informations