Os proxies ES2015 (no Chrome 49 e versões mais recentes) fornecem ao JavaScript uma API de intercessão, permitindo que capturemos ou interceptemos todas as operações em um objeto de destino e modifiquemos como esse destino opera.
Os proxies têm muitos 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 de proxy que recebe 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 gerenciador, que pode modificar o comportamento original do objeto de destino de várias maneiras úteis. O gerenciador contém métodos de trap opcionais (por exemplo, .get()
, .set()
, .apply()
) chamados quando a operação correspondente é realizada no proxy.
Interceptação
Vamos começar com um objeto simples e adicionar um middleware de intercepção a ele usando a API Proxy. O primeiro parâmetro transmitido ao construtor é o destino (o objeto que está sendo usado como proxy) e o segundo é o gerenciador (o próprio proxy). É aqui que podemos adicionar hooks para nossos getters, setters ou outros comportamentos.
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 correta de get ou set de propriedade no objeto proxy resultou em uma chamada de metanível para a captura correspondente no gerenciador. As operações do gerenciador incluem leituras de propriedade, atribuição de propriedade e aplicação de função, que são encaminhadas para a trap correspondente.
A função de trap pode, se assim escolher, implementar uma operação de forma arbitrária (por exemplo, encaminhando a operação para o objeto de destino). Isso é o que acontece por padrão se uma armadilha não for especificada. Por exemplo, aqui está um proxy de encaminhamento sem operação que faz exatamente isso:
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);
Analisamos o uso de proxy para objetos simples, mas também podemos usar proxy para um objeto de função, em que uma função é o 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 sabemos, quando aplicados a dois objetos, esses operadores comparam as identidades dos objetos. O próximo exemplo demonstra esse comportamento. A comparação de dois proxies diferentes retorna falso, mesmo que as segmentações sejam as mesmas. Da mesma forma, o objeto de destino é diferente de qualquer um dos proxies:
// Continuing previous example
var proxy2 = new Proxy (sum, handler);
(proxy==proxy2); // false
(proxy==sum); // false
O ideal é que não seja possível distinguir um proxy de um objeto não proxy, para que a instalação de um proxy não afete o resultado do app. Essa é uma das razões pelas quais 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. Muitas das opções acima, como o controle de acesso e o perfil, se enquadram em wrappers genéricos: proxies que agrupam outros objetos no mesmo "espaço" de endereço. A virtualização também foi mencionada. Objetos virtuais são proxies que emulam outros objetos sem que eles precisem estar no mesmo espaço de endereço. Exemplos incluem objetos remotos (que emulam objetos em outros espaços) e futuros transparentes (que emulam resultados que ainda não foram computados).
Proxies como gerenciadores
Um caso de uso bastante comum para processadores de proxy é realizar verificações de validação ou de controle de acesso antes de realizar uma operação em um objeto encapsulado. A operação só será encaminhada 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 considerar todas as diferentes operações que os manipuladores de proxy podem interceptar. É possível imaginar uma implementação que precisa duplicar o padrão de verificação de acesso e encaminhar a operação em cada armadilha.
Isso pode ser complicado de abstrair, já que cada operação pode precisar ser encaminhada de maneira diferente. Em um cenário perfeito, se todas as operações pudessem ser direcionadas uniformemente por apenas uma armadilha, o gerenciador só precisaria realizar a verificação de validação uma vez na armadilha. Para isso, implemente o gerenciador de proxy como um proxy. Infelizmente, isso está fora do escopo deste artigo.
Extensão de objeto
Outro caso de uso comum para proxies é estender ou redefinir a semântica das operações em objetos. Por exemplo, você pode querer que um gerenciador registre operações, notifique observadores, gere exceções em vez de retornar indefinido ou redirecione operações para diferentes destinos de 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 transmitir o proxy envolvido em uma espécie de membrana protetora. Quando o app considera 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 esse desprendimento recursivamente a todos os objetos acessíveis a partir do destino original definido.
Como usar a reflexão com proxies
Reflect é um novo objeto integrado que fornece métodos para operações JavaScript interceptáveis, muito útil para trabalhar com proxies. Na verdade, os métodos de reflexão são iguais aos dos gerenciadores de proxy.
Linguagens com tipagem estática, como Python ou C#, oferecem há muito tempo uma API de reflexão, mas o JavaScript não precisa de uma, já que é uma linguagem dinâmica. É possível argumentar que o ES5 já tem alguns recursos de reflexão, como Array.isArray()
ou Object.getOwnPropertyDescriptor()
, que seriam considerados reflexão em outros idiomas. A ES2015 apresenta uma API de reflexão que vai abrigar métodos futuros para essa categoria, facilitando a análise deles. Isso faz sentido, porque o objeto é destinado a ser um protótipo básico, e não um bucket para métodos de reflexão.
Usando o Reflect, podemos melhorar nosso exemplo anterior de super-herói para interceptar corretamente os campos nas armadilhas de leitura e gravação 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
Que gera:
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 quer:
Envolve uma definição de proxy dentro de um construtor personalizado para evitar a criação manual de um novo proxy sempre que quisermos trabalhar com uma lógica específica.
Adicionar a capacidade de "salvar" as alterações, mas somente se os dados tiverem sido modificados (hipoteticamente, devido à operação de salvamento ser muito cara).
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 polido de Object.observe()
Embora estejamos dizendo adeus a Object.observe()
, agora é possível polyfill-los usando Proxies ES2015. Recentemente, Simon Blackwell escreveu um shim Object.observe() baseado em proxy que vale a pena conferir. Erik Arvidsson também escreveu uma versão completa de especificação em 2012.
Suporte ao navegador
Os proxies ES2015 são compatíveis com o Chrome 49, o Opera, o Microsoft Edge e o Firefox. O Safari recebeu indicadores públicos mistos sobre o recurso, mas continuamos otimistas. O Reflect está disponível 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 em que um proxy é criado.
Leitura adicional
- ES6 em detalhes: proxies e reflect
- MDN: Proxies ES6
- Proxies ES6 e Reflect na TagTree
- MDN: O objeto Reflect (em inglês)
- Reflexão detalhada sobre o ES6
- Proxies: princípios de design para APIs de intercessão robustas orientadas a objetos
- 2ality: Metaprogramming with ES6 (em inglês)
- Metaprogramação no ES6 usando Reflect
- ES6 everyday Reflect (em inglês)