Présentation des proxys ES2015

Addy Osmani
Addy Osmani

Les proxys ES2015 (dans Chrome 49 et versions ultérieures) fournissent JavaScript avec 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 le fonctionnement de cette cible.

Les proxys 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 proxy qui accepte un objet cible désigné et un objet 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 trap facultatives (par exemple, .get(), .set(), .apply()) appelées lorsque l'opération correspondante est effectuée sur le proxy.

Interception

Commençons par prendre un objet brut 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 traité 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, 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 dans la pratique, l'exécution correcte de la propriété "get" ou de la définition de la propriété sur l'objet proxy a généré un appel de méta-niveau vers l'arrêt 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 fonction, qui sont toutes transmises à l'interruption correspondante.

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

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 simples par proxy, mais nous pouvons tout aussi facilement transmettre un objet fonction par 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 des objets. L'exemple suivant illustre ce comportement. La comparaison de deux proxys distincts renvoie la valeur "false" bien que les cibles sous-jacentes soient identiques. Dans le même ordre d'idées, 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 afin 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 et ne fournit pas d'interruptions pour toutes les opérations sur les objets.

Cas d'utilisation

Comme indiqué précédemment, les proxys sont destinés à un large éventail de cas d'utilisation. La plupart des solutions ci-dessus, telles que le contrôle des accès et le profilage, sont classées dans 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 doivent se trouver dans le même espace d'adressage. Les exemples incluent les objets distants (qui émulent des objets dans d'autres espaces) et les objets futurs transparents (émulant des résultats qui ne sont pas encore calculés).

Les proxys en tant que gestionnaires

Un cas d'utilisation assez courant des gestionnaires 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 aboutit. 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 tenir compte de toutes les différentes opérations que les gestionnaires de proxy peuvent intercepter. On pourrait imaginer une implémentation devant dupliquer le modèle de vérification d'accès et de transfert de l'opération dans chaque piège.

Cette opération peut s'avérer difficile à extraire, é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 de manière uniforme par le biais d'un seul piège, le gestionnaire n'aurait besoin d'effectuer le contrôle de validation qu'une seule fois dans le piège. Pour ce faire, vous pouvez implémenter le gestionnaire de proxy lui-même en tant que proxy. Cela n'est malheureusement 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. Vous pouvez, par exemple, vouloir qu'un gestionnaire consigne les opérations, notifie les observateurs, génère des exceptions au lieu de renvoyer des valeurs 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 proxys. Plutôt que de transmettre un objet cible à un code non approuvé, on peut transmettre son proxy encapsulé dans une sorte de membrane de protection. Lorsque l'application estime que le code non approuvé a effectué une tâche particulière, elle peut révoquer la référence qui dissocie le proxy de sa cible. La membrane étendrait ce décollement de manière récursive à tous les objets accessibles à partir de la cible initiale 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. Il est particulièrement utile pour travailler avec des proxys. En fait, les méthodes Reflect sont identiques à celles des gestionnaires 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 que ce langage soit un langage dynamique. On peut dire que ES5 possède déjà un certain nombre de caractéristiques de réflexion, comme Array.isArray() ou Object.getOwnPropertyDescriptor(), qui seraient considérées comme une réflexion dans d'autres langues. ES2015 introduit une API Reflection qui hébergera les futures méthodes pour cette catégorie, ce qui les rend plus faciles à comprendre. C'est logique, car l'objet est censé être un prototype de base plutôt qu'un bucket pour les méthodes de réflexion.

À l'aide de Reflect, nous pouvons améliorer notre exemple précédent de super-héros pour une interception correcte du champ 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

Résultats:

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 proxy chaque fois que vous souhaitez utiliser une logique spécifique.

  • Ajouter la possibilité d'« enregistrer » les modifications, mais seulement si les données ont été réellement modifiées (hypothétiquement en raison du coût très coûteux 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 page Proxies ES6 de Tagtree.

Polyfilling Object.observe()

Bien que nous ayons adieu à Object.observe(), il est désormais possible de les polyfill à l'aide de proxys ES2015. Simon Blackwell a récemment écrit un shim Object.observe() basé sur un proxy qui mérite d'être examiné. 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 mitigés ont été exprimés dans Safari, 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 que les propriétés de proxy connues au moment de la création d'un proxy.

Complément d'informations