Introdução aos proxies ES2015

Addy Osmani
Addy Osmani

Os proxies ES2015 (no Chrome 49 e versões posteriores) fornecem JavaScript com uma API de intercessão, o que nos permite interceptar ou interceptar todas as operações em um objeto de destino e modificar a forma como esse destino opera.

Os proxies têm um grande número de usos, incluindo:

  • Interceptação
  • Virtualização de objetos
  • Gerenciamento de recursos
  • Criação de perfil ou geração de registros para depuração
  • Segurança e controle de acesso
  • Contratos de uso de objetos

A API Proxy contém um construtor Proxy que usa um objeto de destino designado e um objeto de gerenciador.

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

O comportamento de um proxy é controlado pelo Handler, que pode modificar o comportamento original do objeto target de diversas maneiras úteis. O gerenciador contém métodos de captura opcionais (por exemplo, .get(), .set(), .apply()) chamados quando a operação correspondente é executada no proxy.

Interceptação

Vamos começar com um objeto simples e adicionar um middleware de interceptação a ele usando a API Proxy. Lembre-se de que o primeiro parâmetro passado para o construtor é o destino (o objeto que está sendo encaminhado) e o segundo é o manipulador (o próprio proxy). É aqui que podemos adicionar hooks para nossos getters, setters ou outro comportamento.

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);

Ao executar o código acima no Chrome 49, temos o seguinte:

get was called for: power  
"Flight"

Como podemos ver na prática, a execução de nossa propriedade get ou propriedade definida no objeto proxy resultou corretamente em uma chamada de nível meta para a armadilha correspondente no gerenciador. As operações do gerenciador incluem leituras de propriedades, atribuição de propriedades e aplicação de funções, e todas elas são encaminhadas para a armadilha correspondente.

Se quiser, a função de captura pode implementar uma operação arbitrariamente (por exemplo, encaminhando a operação para o objeto de destino). Isso é o que acontece por padrão se uma armadilha não é especificada. Por exemplo, este é um proxy de encaminhamento de ambiente autônomo que faz exatamente isto:

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);

Acabamos de usar um proxy de objetos simples, mas podemos usar um objeto de função com a mesma facilidade, em que uma função é nosso destino. Desta vez, vamos usar a armadilha 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

Como identificar proxies

A identidade de um proxy pode ser observada usando os operadores de igualdade do JavaScript (== e ===). Como já sabemos, quando aplicados a dois objetos, esses operadores comparam as identidades de objetos. O próximo exemplo demonstra esse comportamento. A comparação de dois proxies diferentes retorna "false", mesmo que os destinos sejam os mesmos. Na mesma linha, o objeto de destino é diferente de qualquer um de seus proxies:

// Continuing previous example

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

O ideal é que você não consiga distinguir um proxy de um objeto que não seja de proxy, para que a implementação de um proxy não afete o resultado do app. Esse é um motivo pelo qual a API Proxy não inclui uma maneira de verificar se um objeto é um proxy nem fornece armadilhas para todas as operações em objetos.

Casos de uso

Como mencionado, os proxies têm uma ampla variedade de casos de uso. Muitos desses elementos, como controle de acesso e criação de perfil, se enquadram em wrappers genéricos: proxies que unem outros objetos em um mesmo "espaço" de endereço. A virtualização também foi mencionada. Os objetos virtuais são proxies que emulam outros objetos sem que eles precisem estar no mesmo espaço de endereço. Entre os exemplos estão objetos remotos (que emulam objetos em outros espaços) e futuros transparentes (emulando resultados que ainda não foram calculados).

Proxies como gerenciadores

Um caso de uso muito comum para gerenciadores de proxy é realizar verificações de validação ou controle de acesso antes de realizar uma operação em um objeto encapsulado. A operação será encaminhada somente se a verificação for bem-sucedida. O exemplo de validação abaixo demonstra isso:

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

Exemplos mais complexos desse padrão podem levar em consideração todas as diferentes operações que manipuladores de proxy podem interceptar. Podemos imaginar uma implementação tendo que duplicar o padrão de verificação de acesso e encaminhar a operação em cada armadilha.

Isso pode ser difícil de abstrair, já que cada op pode ter que ser encaminhada de modo diferente. Em um cenário perfeito, se todas as operações pudessem ser conduzidas de maneira uniforme por meio de apenas uma armadilha, o gerenciador só precisaria realizar a verificação de validação uma vez na armadilha. Você pode fazer isso implementando o gerenciador de proxy como um proxy. Infelizmente, isso está fora do escopo deste artigo.

Extensão de objeto

Outro caso de uso comum de proxies é a extensão ou a redefinição da semântica de operações em objetos. Por exemplo, é possível querer que um gerenciador registre operações, notifique observadores, lance exceções em vez de retornar indefinidos ou redirecione operações para diferentes destinos para armazenamento. Nesses casos, o uso de um proxy pode levar a um resultado muito diferente do uso do objeto de destino.

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

Controle de acesso

O controle de acesso é outro bom caso de uso para proxies. Em vez de transmitir um objeto de destino para um código não confiável, é possível passar o proxy envolto em uma espécie de membrana protetora. Depois que o app considerar que o código não confiável concluiu uma tarefa específica, ele pode revogar a referência que separa o proxy do destino. A membrana estenderia essa separação de maneira recursiva para todos os objetos acessíveis do alvo original definido.

Como usar reflexão com proxies

Reflect é um novo objeto integrado que fornece métodos para operações JavaScript interceptáveis, muito úteis para trabalhar com proxies. Na verdade, os métodos Reflect são os mesmos dos gerenciadores de proxy.

Linguagens com tipo estático, como Python ou C#, oferecem há muito tempo uma API de reflexão, mas o JavaScript não precisava mesmo sendo uma linguagem dinâmica. Pode-se argumentar que o ES5 já tem alguns recursos de reflexão, como Array.isArray() ou Object.getOwnPropertyDescriptor(), que seriam considerados reflexos em outras linguagens. O ES2015 introduz uma API Reflection que vai armazenar métodos futuros para essa categoria, facilitando o raciocínio. Isso faz sentido, já que o objeto é um protótipo de base e não um bucket para métodos de reflexão.

Com o recurso Reflect, podemos melhorar nosso exemplo anterior de super-herói para interceptação de campo adequada em nossas armadilhas de get e set, da seguinte maneira:

// 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

O que resulta:

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

Outro exemplo é quando alguém pode querer:

  • Una uma definição de proxy em um construtor personalizado para evitar a criação manual de um novo proxy sempre que quisermos trabalhar com uma lógica específica.

  • Adicione a capacidade de "salvar" alterações, mas somente se os dados realmente tiverem sido modificados (hipoteticamente devido ao alto custo da operação de salvamento).

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();

Para mais exemplos da API Reflect, consulte Proxies ES6 da Tagtree.

Preenchimento de Object.observe()

Com o adeus em Object.observe(), agora é possível fazer polyfills usando proxies ES2015. Simon Blackwell escreveu um shim baseado em Proxy que vale a pena conferir recentemente. Erik Arvidsson também escreveu uma versão bastante completa desde 2012.

Suporte ao navegador

Os proxies ES2015 são compatíveis com Chrome 49, Opera, Microsoft Edge e Firefox. O Safari teve indicadores públicos mistos em relação ao recurso, mas permanecemos otimistas. O Reflect está no Chrome, Opera e Firefox e está em desenvolvimento para o Microsoft Edge.

O Google lançou um polyfill limitado para proxy. Isso só pode ser usado para wrappers genéricos, já que só pode usar propriedades de proxy conhecidas no momento da criação de um proxy.

Leia mais