Objeto Proxy em JavaScript – Case de Uso

Objeto Proxy em JavaScript.

O objeto Proxy em JavaScript é usado para definir comportamentos customizados em operações fundamentais, como leitura de propriedade, atribuição, invocação de funções, etc.

Mas o que você quer dizer ao falar em comportamentos customizados? Bom, quando se faz um get a uma propriedade, qual é o comportamento esperado? O retorno do conteúdo da propriedade, certo? Perfeito!

Com o objeto Proxy, você pode monitorar uma propriedade específica ou função, e disparar a execução de um código arbitrário quando uma operação de leitura ou atribuição, por exemplo, é feita sobre ela.

Ou seja, um get, que naturalmente retornaria uma informação, pode ser alterado para executar um código definido por mim.

Vamos lá entender melhor como funciona essa história e para que raios isso é útil. E olha que isso é muito mais utilizado do que você imagina.

A prática tem o poder de facilitar muito o entendimento das coisas. Por isso, vou trazer um exemplo de uso bem interessante do objeto Proxy, que encontrei no livro Cangaceiro JavaScript de Flávio Almeida.

Portanto, os próximos parágrafos explicarão os arquivos do projeto que iremos utilizar como base para demonstrar códigos e, após esta etapa, já teremos condições de começar a abordar o funcionamento e uso de um Proxy.

Um projeto para experimentos

Você pode baixar os arquivos do projeto que utilizaremos no decorrer do artigo para praticar e, ao seguir os exemplos, visualizar como resultado o código funcionando.

O projeto trata-se de uma página para cadastro de clientes. No modelo ou cerne da aplicação, temos a representação de um contato para o nosso sistema, através de uma classe de mesmo nome.

class Contato {
	constructor(nome, email, telefone) {
		this._nome = nome;
		this._email = email;
		this._telefone = telefone;
	}

	get nome() {
		return this._nome;
	}

	get email() {
		return this._email;
	}

	get telefone() {
		return this._telefone;
	}
}

Também em models, um objeto ListaDeContatos que, como o próprio nome sugere, irá guardar a lista com os contatos que forem sendo inseridos.

class ListaDeContatos {

	constructor() {
		this._contatos = [];
	}

	adiciona(contato) {
		this._contatos.push(contato);
	}

	apaga() {
		this._contatos = [];
	}

	get contatos() {
		return this._contatos;
	}
}

Para a view, criamos um template caseiro, utilizando o recurso de Template Strings do JavaScript. Mas poderia ter sido usado algum framework, como o React.

Este template é uma classe, que recebe em seu construtor o elemento do DOM onde o template deve ser renderizado na página html.

O método _template da classe, retorna uma Template String. Utilizamos um underline _ antes do seu nome, pois esta é uma convenção para nomenclatura de métodos "privados", que somente deveriam ser acessados por métodos da própria classe.

A ideia foi gerar uma string com marcação html válida, para que pudesse ser passada posteriormente à propriedade innerHTML e resultasse em elementos html de fato. O que foi feito no método update da mesma classe.

Expressões embutidas em Template Strings são executadas primeiro. Por isso, o array com a lista de contatos recebido pelo _template é transformado, através de um map, em um array de Template Strings, onde cada ítem é uma string com sintaxe html válida, representando uma nova linha da futura tabela.

Mas um array de strings não serve. Para este cenário, precisamos de uma string única, a fim de executar um innerHTML sobre ela. Convertendo a string em elementos html.

Por isso aplicamos, sobre o array de strings resultante do map, um join, informando o critério para junção como sendo uma string em branco.

No index.html, temos o elemento onde a template deve entrar, e o formulário para interação do usuário com a página.

Em ContatosController, criamos um alias (apelido) para digitar menos código, através de um recurso bem bacana do JavaScript que são as bound functions. Você pode entender melhor sobre elas neste vídeo.

Na sequência, instanciamos nossa lista de contatos, sua view e adicionamos alguns contatos para que algo possa já ser exibido inicialmente ao usuário.

Depois pedimos para a view se atualizar com o novo estado da lista de contatos e buscamos os elementos no DOM, correspondentes aos campos por onde o usuário pode "inputar" dados.

class ContatosController {

	constructor() {

		/* Criando um "alias", para não ter que digitar toda vez document.querySelector ao
		precisar buscar um elemento no DOM */
		let $ = document.querySelector.bind(document);

		this._listaDeContatos = new ListaDeContatos();
		this._contatosView = new ContatosView($('.tabela'));

		//Criando alguns contatos somente para ter algo na tabela.
		this._listaDeContatos.adiciona(new Contato('João', 'joao@email.com', '(43)984624376'));
		this._listaDeContatos.adiciona(new Contato('Emily', 'emily@email.com', '(41)994561432'));
		this._listaDeContatos.adiciona(new Contato('Tiago', 'tiago@email.com', '(43)980304527'));
		this._listaDeContatos.adiciona(new Contato('Valeska', 'valeska@email.com', '(48)973026387'));

		//Pedindo para a View se atualizar
		this._contatosView.update(this._listaDeContatos);

		//Buscando os campos no DOM
		this._inputNome = $('#nome');
		this._inputEmail = $('#email');
		this._inputTelefone = $('#telefone');
	}

	adiciona(event) {

		event.preventDefault();
		this._listaDeContatos.adiciona(this._criaContato());
		this._contatosView.update(this._listaDeContatos);
		this._limpaFormulario();
	}

	apaga() {

		this._listaDeContatos.apaga();
		this._contatosView.update(this._listaDeContatos);
	}

	_criaContato() {

		return new Contato(
			this._inputNome.value,
			this._inputEmail.value,
			this._inputTelefone.value);
	}

	_limpaFormulario() {

		this._inputNome.value = '';
		this._inputEmail.value = '';
		this._inputTelefone.value = '';
		this._inputNome.focus();
	}	
}

No método adiciona, cancelamos a ação padrão de recarregar a página e limpar o formulário quando clica-se no botão para submeter o formulário. Sem cancelar este comportamento, o formulário acaba sendo limpo antes mesmo de pegarmos os valores contidos nele para fazer os devidos tratamentos.

Em seguida, adicionamos o contato criado através do método interno _criaContato() na lista e pedimos novamente para que a view se atualize com o novo estado da lista de contatos.

Já que cancelamos a ação padrão através de event.preventDefault(), o que incluía limpar os campos do formulário, invocamos o método privado _limpaFormulario() para executar esta ação após o contato já ter sido salvo.

Ok, projeto apresentado. Vamos agora passar a entender onde o objeto Proxy entra nessa história.

Onde utilizamos o objeto Proxy?

Para começar, gostaria de lhe convidar a dar uma olhada na classe ContatosController. Repare que: toda vez que uma atualização de view é requerida, o programador precisa fazê-la manualmente. Por exemplo: quando acontece a inserção ou exclusão de pessoas na lista, lá vai o programador e invoca o método update(), passando o novo estado da lista de contatos. Através da seguinte linha de código:

this._contatosView.update(this._listaDeContatos);

Temos ainda somente um template para ser atualizado. Imagine a hora que esta aplicação crescer, você vai lembrar de chamar a atualização da view todas as vezes que se fizer necessário? Nem eu... Por isso, nossa aplicação deve ser capaz de saber quando alguma view precisa ser atualizada.

No caso da ListaDeContatos, a view deve ser atualizada quando inserimos ou apagamos os contatos.

class ListaDeContatos {

	constructor() {
		this._contatos = [];
	}

	adiciona(contato) {
		this._contatos.push(contato);
		//Atualizar a view
	}

	apaga() {
		this._contatos = [];
		//Atualizar a view
	}

	get contatos() {
		return this._contatos;
	}
}

Isso poderia ser feito assim: estando em ContatosController, passamos uma função para dentro de ListaDeContatos via construtor. Essa função irá conter justamente o método update().

O método ainda continuará recebendo a lista de contatos. Então a função recebe esse parâmetro e o delega ao método.

class ContatosController {

	constructor() {

		let $ = document.querySelector.bind(document);

		this._listaDeContatos = new ListaDeContatos(listaDeContatos =>
			this._contatosView.update(listaDeContatos)
		);

		this._contatosView = new ContatosView($('.tabela'));

		//Código posterior omitido.

Agora em ListaDeContatos, vamos receber a função pelo construtor com o nome de armadilha (trap). Pois este é o nome que você encontrará nas documentações do Proxy. Na sequência, invocá-la nos métodos sinalizados acima. A saber: adiciona e apaga.

A lista de contatos – que o método update precisa para funcionar – será exatamente o this. Pois já estamos no próprio contexto de ListaDeContatos.

class ListaDeContatos {

	constructor(armadilha) {
		this._contatos = [];
		this._armadilha = armadilha;
	}

	adiciona(contato) {
		this._contatos.push(contato);

		//Atualizar a view
		this._armadilha(this);
	}

	apaga() {
		this._contatos = [];

		//Atualizar a view
		this._armadilha(this);
	}

	get contatos() {
		return this._contatos;
	}
}

A função foi declarada no contexto de ContatosController, mas será executada dentro de ListaDeContatos. Portanto, para que o this, dentro do escopo da função, continue sendo ContatosController precisamos usar uma Arrow Function. Do contrário, this adotará o contexto de invocação da função, que no caso, será ListaDeContatos.

Com isso, em ContatosController, já podemos remover todas as chamadas ao método update() que tinham por objetivo, atualizar a view de contatos.

class ContatosController {

	constructor() {

		let $ = document.querySelector.bind(document);

		this._listaDeContatos = new ListaDeContatos(listaDeContatos =>
			this._contatosView.update(listaDeContatos) //O this desta linha está sob o escopo da função.
		);

		this._contatosView = new ContatosView($('.tabela'));

		this._listaDeContatos.adiciona(new Contato('João', 'joao@email.com', '(43)984624376'));
		this._listaDeContatos.adiciona(new Contato('Emily', 'emily@email.com', '(41)994561432'));
		this._listaDeContatos.adiciona(new Contato('Tiago', 'tiago@email.com', '(43)980304527'));
		this._listaDeContatos.adiciona(new Contato('Valeska', 'valeska@email.com', '(48)973026387'));

		this._inputNome = $('#nome');
		this._inputEmail = $('#email');
		this._inputTelefone = $('#telefone');
	}

	adiciona(event) {
		event.preventDefault();
		this._listaDeContatos.adiciona(this._criaContato());
		this._limpaFormulario();
	}

	apaga() {
		this._listaDeContatos.apaga();
	}

	_criaContato() {
		return new Contato(
			this._inputNome.value,
			this._inputEmail.value,
			this._inputTelefone.value);
	}

	_limpaFormulario() {
		this._inputNome.value = '';
		this._inputEmail.value = '';
		this._inputTelefone.value = '';
		this._inputNome.focus();
	}
}

Tudo bem. Agora nossa aplicação já sabe atualizar a view por conta própria. Mas pensa comigo: é uma boa "sujar" nosso modelo (que deveria ser a parte mais reaproveitável de uma aplicação e conter somente as regras de negócio) colocando dentro dele código de infraestrutura ou referente à view? A resposta geralmente é não. Além do mais, se resolvessemos em algum momento no futuro parar de utilizar nossas templates caseiras, migrando para um framework como o React, teríamos que novamente "mexer" em nossas classes do modelo para consertar as views.

Como esta – de cara – não é uma boa solução, vamos retornar nosso modelo ao que ele era antes. Mantendo nele somente as regras de negócio. No controller, também não invocaremos mais as atualizações da view.


class ListaDeContatos {

	constructor() {
		this._contatos = [];
	}

	adiciona(contato) {
		this._contatos.push(contato);
	}

	apaga() {
		this._contatos = [];
	}

	get contatos() {
		return this._contatos;
	}
}

Assim nossa aplicação ficou sem a view. Mas vamos resolver este impasse usando proxy, e quando detectarmos a invocação dos métodos adiciona ou apaga da nossa classe ListaDeContatos, daremos um jeito de atualizar a view para o usuário.

Entendendo o Objeto Proxy

O Padrão de Projeto Proxy já está implementado em JavaScript. Tudo o que você precisa fazer para usá-lo, é instanciar um proxy.

No console do navegador em nossa aplicação, podemos instanciar uma pessoa, por exemplo:

Criando uma instância da classe Contato no console do navegador.
let contato = new Contato(
    'Suzete',
    'suzete@gmail.com',
    '(41)999-999-999'
);

E posteriormente, acessar alguma propriedade dela, tipo o nome.

contato.nome; //"Suzete"

Mas também, podemos encapsular o objeto em uma proxy. Assim:

let contato = new Proxy(
	new Contato(
		'Suzete',
		'suzete@gmail.com',
		'(41)999-999-999'
	),
{});

E o resultado ao invocar a propriedade nome será o mesmo.

contato.nome; //"Suzete"

Isso significa que: encapsulando um objeto dentro de um proxy, você ainda terá acesso a ele, mas deve passar primeiro pelo proxy. Ou seja, o proxy delega as ações para o objeto original encapsulado. É nesse momento que podemos pedir ao proxy que execute determinado código arbitrário, antes de delegar uma ação ao objeto original.

Ao instanciar um proxy você passa, como primeiro parâmetro, o objeto a ser encapsulado. O segundo, é um handler (lidador). Onde estarão as armadilhas. O handler é declarado em forma de objeto literal.

Disparando Armadilhas na Leitura de Propriedades

Nossa classe Contato possui getters para nome, email e telefone.

class Contato {
	constructor(nome, email, telefone) {
		this._nome = nome;
		this._email = email;
		this._telefone = telefone;
	}

	get nome() {
		return this._nome;
	}

	get email() {
		return this._email;
	}

	get telefone() {
		return this._telefone;
	}
}

Então vamos colocar a armadilha que dispara quando um get for feito a alguma das propriedades.

A função lidadora do proxy para interceptar operações de leitura em propriedades chama-se get, e recebe três parâmetros: target, prop e receiver.

O target é o objeto real, encapsulado pelo proxy. Prop, a propriedade que está sendo lida. E o receiver, uma referência ao próprio proxy.

Na tira de código acima, fizemos um get nas propriedades nome e email do contato instanciado. Dessa forma, nossa armadilha para getters foi acionada. Porém, ela só executa um console log dizendo qual propriedade foi interceptada. Não é implícito ao proxy que após a execução da armadilha ele deve seguir o fluxo chamando o getter. Somos nós programadores que definimos isso através de um return. Como não dissemos qual seria o retorno após a execução da armadilha, ele foi undefined.

Podemos definir qualquer coisa como retorno. Veja:

Ou fazer uma operação de leitura na propriedade interceptada, que é justamente o retorno esperado por quem invocou o nosso get responsável por disparar a armadilha.

A leitura na propriedade é feita com Reflect.get(), onde target é o objeto original encapsulado pela proxy, prop refere-se à propriedade que estou acessando, e receiver é uma referência a meu proxy.

As propriedades da nossa classe Contato nada mais fazem do que um get nas propriedades do construtor da classe. Por isso que a armadilha dispara duas vezes.

Disparando Armadilhas na Modificação de Propriedades

Podemos ter interesse também, em executar uma armadilha quando algo é atribuido. Seja a uma propriedade ou método.

Com isso em mente, para que tenhamos um exemplo, vamos criar um setter denominado sobrenome, em nossa classe Contato.

class Contato {
	constructor(nome, email, telefone) {
		this._nome = nome;
		this._sobrenome;
		this._email = email;
		this._telefone = telefone;
	}

	get nome() {
		return this._nome;
	}

	set sobrenome(sobrenome) {
		this._sobrenome = sobrenome;
	}

	get email() {
		return this._email;
	}

	get telefone() {
		return this._telefone;
	}
}

E adaptando nossa armadilha para interceptar propriedades de atribuição, alteramos a função de get para set. Essa nova função também recebe um parâmetro extra, chamado value. E claro, depois de executar a armadilha, passamos os parâmetros que a nossa propriedade sobrenome do objeto real está esperando, através do Reflect.set().

Mas, o proxy implementado pelo JavaScript não está preparado para interceptar métodos. E agora, o que fazer para interceptar nossos métodos adiciona e apaga no projeto que estamos usando de exemplo? Vamos para uma implementação caseira.

Disparando Armadilhas na Invocação de Métodos

Quando chamamos um método ou função, o JavaScript internamente executa um get e depois manda um apply – através da API de reflexão – com os parâmetros que o método ou função está esperando.

Então em nossa classe ContatosController, encapsulamos a ListaDeContatos em um Proxy;

class ContatosController {

	constructor() {

		let $ = document.querySelector.bind(document);

		this._listaDeContatos = new Proxy(new ListaDeContatos(), {});
    
    //Restante do código omitido

No lidador do Proxy, colocamos a armadilha que dispara em operações de leitura;

this._listaDeContatos = new Proxy(new ListaDeContatos(), {
	get(target, prop, receiver) {

	}
});

Ao interceptar um getter, ele pode ser uma propriedade, método ou função. Dessa forma, antes de executar o código da minha armadilha, verifico se o que interceptei foi um método ou função.

Checo através de um if, comparando o typeof do getter interceptado com o de function. Isso porque, o typeof de um método ou função é function. Como a propriedade – prop – que estamos avaliando em nosso objeto – target – varia, usamos a notação de colchetes para deixar isso flexível.

Se tratando de um método ou função, ainda preciso ter em mente que não é para qualquer deles que irei disparar minha armadilha. Uma forma de filtrar, poderia ser utilizando o método includes() do objeto Array.

this._listaDeContatos = new Proxy(new ListaDeContatos(), {
	get(target, prop, receiver) {
		if(typeof(target[prop]) == typeof(Function) && ['adiciona', 'apaga'].includes(prop)) {
			//Passou nas validações do if? Executa o que está aqui.
		}
	}
});

Uma vez que trata-se do método de interesse, vamos retornar uma função, que irá substituir (na instância do objeto encapsulado pelo proxy) o método interceptado.

Dentro dessa função, vamos chamar o método do objeto original que foi interceptado, através do método estático apply() da classe Reflect, atribuindo o contexto (this) e os arguments.

O arguments é necessário para métodos interceptados que passam parâmetros. Isso porque, a armadilha de leitura get do nosso lidador não dá acesso ao value, onde os parâmetros estaríam disponíveis.

class ContatosController {

	constructor() {

		let $ = document.querySelector.bind(document);

		this._listaDeContatos = new Proxy(new ListaDeContatos(), {
			get(target, prop, receiver) {
				if(typeof(target[prop]) == typeof(Function) && ['adiciona', 'apaga'].includes(prop)) {
					return function() {
						Reflect.apply(target[prop], target, arguments);

					}
				}
			}
		});

E por último, atualizar a view. Aqui devemos salvar o this em uma variável externa, para garantir que _contatosView.update seja executado no contexto de ContatosController.

class ContatosController {

	constructor() {

		let $ = document.querySelector.bind(document);

		let self = this;
		this._listaDeContatos = new Proxy(new ListaDeContatos(), {
			get(target, prop, receiver) {
				if(typeof(target[prop]) == typeof(Function) && ['adiciona', 'apaga'].includes(prop)) {
					return function() {
						Reflect.apply(target[prop], target, arguments);
						self._contatosView.update(target);
					}
				}
			}
		});

Caso o fluxo não tenha entrado em nosso if, significa que o getter interceptado foi então uma propriedade. Nesse caso, executamos simplesmente o get na propriedade, e deixamos a coisa seguir.

class ContatosController {

	constructor() {

		let $ = document.querySelector.bind(document);

		let self = this;
		this._listaDeContatos = new Proxy(new ListaDeContatos(), {
			get(target, prop, receiver) {

				if(typeof(target[prop]) == typeof(Function) && ['adiciona', 'apaga'].includes(prop)) {
					return function() {
						Reflect.apply(target[prop], target, arguments);
						self._contatosView.update(target);
					}
				}

				return Reflect.get(target, prop, receiver);
			}
		});

Recapitulando: a função retornada pela armadilha substitui o método interceptado na instância de lista de contatos encapsulada pelo proxy. Essa função faz então a chamada ao método do objeto real, através de Reflect.get e, na sequência, atualiza a view com o novo estado da lista.

Nosso código final ficou assim: agora não precisamos mais nos preocupar com a view, pois quando os métodos adiciona e apaga da ListaDeContatos forem chamados, a view se atualizará sozinha.

A ideia aqui foi demonstrar um exemplo bastante prático, para que você possa também usar esta técnica em suas aplicações. Caso você queira ir mais a fundo e ver, por exemplo, como usar o Proxy para atualizar a view quando faz-se atribuição a uma propriedade, recomendo o livro Cangaceiro JavaScript de Flávio Almeida, que aborda mais a fundo essa técnica, além de outros padrões de projetos bastante populares e difundidos como o Factory, Promise, DAO, Decorator, etc.

Artigos Relacionados

Ir para o topo