JavaScript: Entendendo ES6 Proxies

Examinando a biblioteca `on-change` e seus detalhes

Image for post
Image for post
Créditos da imagem: http://dev-momo.tistory.com/entry/javascript-ES6-Proxy

ES6 Proxies são novas funcionalidades adicionadas ao ES6. Um recurso poderoso, que pode ser usado para resolver vários problemas de forma elegante. Vamos examinar e recriar uma pequena biblioteca criada por @sindresorhus, chamada on-change. O objetivo conceitual é entender como Proxies funcionam e, no processo, construir algo para que os conceitos sejam reforçados.

Eu tentei manter as explicações do modo mais simples possível. No entanto, uma pequena familiaridade com JavaScript é esperada.

O que a biblioteca on-change faz? É um pequeno utilitário que observa alterações em um objeto ou array. Segue abaixo um exemplo:

const onChange = require("on-change");const object = {
foo: false,
a: {
b: [
{
c: false
}
]
}
};
let i = 0;
const logger = () => console.log("Objeto mudou:", ++i);
const watchedObject = onChange(object, logger);watchedObject.foo = true; // [A]
//=> 'Objeto mudou: 1'
watchedObject.a.b[0].c = true; // [B]
//=> 'Objeto mudou: 2'

Alguns pontos à observar:

Então vamos ver como podemos usar ES6 Proxies para recriar essa biblioteca. Mas antes disse, o que é um Proxy?

O que é um Proxy?

Considere o código a seguir:

const someObject = { prop1: 'Awesome' };console.log (someObject.prop1); // Awesome
console.log (someObject.prop2); // undefined

Ao acessar someObject.prop1, teremos Awesome. Mas ao tentar acessar someObject.prop2, teremos undefined, pois prop2 não existe no objeto alvo.

Vamos dizer que queremos retornar um valor padrão, toda vez que uma propriedade inexistente for acessada. Isto é, someObject.prop2 irá retornar um Oops! This property does not exist ao invés de undefined. Como podemos realizar isso sem modificar ou adicionar novas propriedades à someObject?

A resposta é com ES6 Proxies. O Oxford English Dictionary define um Proxy como: “…autoridade para representar outra pessoa…”. Isso é exatamente o que um Proxy em JavaScript faz. Proxies fazem parte da especificação ES6 e nos permitem interceptar operações (como definir um valor ou excluir uma propriedade) executadas em um determinado objeto. Ao acessar a propriedade de um objeto, o seguinte fluxo acontece:

Image for post

Ao usarmos Proxies, o novo fluxo é:

Image for post

Como você pode ver no diagrama acima, o Proxy fica entre o objeto e o programa, mediando a troca de valores. O Proxy pode verificar o objeto para uma determinada chave de propriedade e, se não existir, pode enviar sua própria resposta também.

Tendo mostrado como os Proxies funcionam, vamos ver como podemos criá-los em JavaScript. Alguns termos para você se familiarizar:

Criando Proxies

A primeira coisa que precisamos é de um objeto para o qual estamos criando o Proxy. Vamos criar:

const originalObject = {firstName: 'Arfat', lastName: 'Salman'};

Agora, precisamos pensar quais traps iremos criar. Nesse exemplo, vamos interceptar o get. Nossa trap irá viver no handler, como abaixo:

const handler = {
get(target, property, receiver) {
console.log(`GET ${property}`);
return target[property];
}
};

Algumas coisas para prestar atenção:

Agora, precisamos combinar handler e o originalObject. Fazemos isso usando o construtor Proxy.

const proxiedObject = new Proxy (originalObject, handler);

O código inteiro se parece com:

Image for post

Você pode executá-lo no console do navegador ou com Node.js (versão ≥ 7). Aqui está uma amostra:

Image for post

Agora, se você acessar firstName no proxiedObject, você terá:

console.log (proxiedObject.firstName); 
// => GET firstName
// => Arfat

Teremos dois logs. Se você estiver acompanhando e receber os mesmo logs no seu código, isso significa que tudo está correto! 🎉

Agora, vamos modificar o handler para manipular propriedades inexistentes:

const newHandler = {
get(target, property, receiver) {
console.log(`GET ${property}`);
if (property in target) { // [A]
return target[property];
}
return 'Oops! This property does not exist.';
}
};

Perceba o item // [A]. Estamos verificando se a propriedade existe no objeto alvo. Se existir, retornamos o seu valor. Caso contrário, retornamos Oops! This property does not exist..

Agora, ao testarmos:

console.log(proxiedObject.thisPropertDoesNotExist);
// => GET thisPropertyDoesNotExist
// => Oops! This property does not exist.

Você não receberá undefined, mas sim, a string que definimos.

Observe que, para operações que não possuem traps, elas são passadas o objeto alvo normalmente, como se o Proxy não existisse.

Recriando on-change

Agora que entendemos como Proxies funcionam, estamos prontos para recriar a biblioteca on-change. Como discutimos acima, onChange é uma função que recebe dois parâmetros: o objeto alvo e a função que será executada em cada mudança no objeto. Então, vamos criar uma função:

const onChange = (objToWatch, onChangeFunction) => {};

Por hora, não faz muita coisa.

Vamos recapitular nosso problema: “Queremos executar onChangeFunction sempre que objToWatch for alterado, ou seja, quando uma propriedade for acessada, modificada, excluída ou adicionada".

Fica claro que iremos utilizar Proxies para interceptar operações no objeto. Então, vamos retornar uma Proxy em onChange com um handler vazio. Como não iremos definir nenhuma trap, todas as operações são passadas para o objeto alvo diretamente, ou seja:

const onChange = (objToWatch, onChangeFunction) => { 
const handler = {};
return new Proxy(objToWatch, handler);
};

Agora, vamos nos concentrar em: “quando uma propriedade for acessada”, significa que teremos uma trap para get. Teremos que chamar onChangeFunction antes de retornar o valor da propriedade. Podemos simular o que a biblioteca on-change faz, com o seguinte:

const onChange = (objToWatch, onChangeFunction) => { 
const handler = {
get(target, property, receiver) {
onChangeFunction(); // Calling our function
return target[property];
}
};
return new Proxy(objToWatch, handler);
};

Vamos executá-lo antes de prosseguir:

Image for post

Parece que está funcionando 🎉. Conseguimos realizar uma parte. Vamos focar em: “quando uma nova propriedade é adicionada ou uma propriedade é excluída”. Como já sabemos a base, precisamos apenas adicionar mais traps para realizar o desejado. A trap que define se uma propriedade é modificado é o set. Vamos adicionar isso ao handler:

const onChange = (objToWatch, onChangeFunction) => { 
const handler = {
get(target, property, receiver) {
onChangeFunction();
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
onChangeFunction();
return Reflect.set(target, property, value);
}
};

return new Proxy(objToWatch, handler);
};

set recebe 4 parâmetros: o parâmetro extra, value, é o valor que está sendo definido. Estamos usando Reflect pois ele nos garante uma maneira pragmática de manipular um objeto. Não é tão diferente de obj.name = 'Arfat'.

Você pode ler aqui porque é melhor usar a API Reflect nesse caso. E se quiser saber mais sobre ela, veja a documentação na MDN.

Como estamos usando a API Reflect, iremos substituir também o target[property] pelo método equivalente em get.

Do mesmo modo, se quisermos interceptar a exclusão de uma propriedade, podemos utilizar a trap deleteProperty. Ficando dessa maneira:

const onChange = (objToWatch, onChangeFunction) => { 
const handler = {
get(target, property, receiver) {
onChangeFunction();
return Reflect.get(target, property, receiver);
},
set(target, property, value) {
onChangeFunction();
return Reflect.set(target, property, value);
},
deleteProperty(target, property) {
onChangeFunction();
return Reflect.deleteProperty(target, property);
}
};

return new Proxy(objToWatch, handler);
};

Ao executar esse código:

const logger = () => console.log('I was called');
const obj = { a: 'a' };
const proxy = onChange(obj, logger);
console.log(proxy.a); // logger chamado em `get` trap
proxy.b = 'b'; // logger chamado em `set` trap
delete proxy.a; // logger chamado em `deleteProperty` trap

Você verá I was called 3 vezes. Isso significa que simulamos com sucesso o que a biblioteca on-change faz.

Há! Uma coisa que não expliquei: Se você tiver um objeto aninhado em uma matriz, eles não irão chamar nossa trap. Por exemplo, se você tiver um array: [1, 2, {a: false}] e realizar: array[2].a = true, nossa função handler não irá ser chamada.

É fácil corrigir esse bug. Ao invés de retornar o valor na trap em get, retornamos outro Proxy, caso o valor seja um objeto. Dessa forma, garantimos que nosso Proxy não falhe.

Iremos alterar get para:

get(target, property, receiver) {
onChangeFunction();
const value = Reflect.get(target, property, receiver);
if (typeof value === 'object') {
return new Proxy(value, handler);
}
return value;
}

Agora, nossa solução irá funcionar mesmo com objetos aninhados dentro de array e objetos.

Finalizando

Algumas coisas que on-change faz de maneira diferente:

Como revisamos o que são Proxies e traps, poderíamos adicionar essas opções sem dor de cabeça. Eu passo esse desafio para você. Se quiser dar uma olhada no código fonte de on-change, acesse esse link.

Um outro problema, que também afeta on-change é: Se você tem um array e você faz proxiedArray.sort(), ou qualquer outra função que modifique fortemente o array, a função logger será executada múltiplas vezes. Por exemplo, ao classificar o array [2,3,4,5,6,7,1], nosso logger será chamado 12 vezes. Essa pode ser uma funcionalidade desejada, ou não. Depende do desenvolvedor.

Um outro problema aberto no repositório de on-change, você notará que a trap de get viola algo chamado de Invariant. Invariantes são restrições colocadas em objetos Proxy, pela própria API Proxy. Essas restrições desabilitam operações ilegais em objetos cujos descritores são definidos de uma determinada maneira.

O problema também lista uma potencial solução. Você pode ler as referências abaixo para obter um entendimento mais profundo de Proxye Invariant. Se você nunca contribuiu para código-aberto antes, isso poderia ser um ótimo começo, corrigindo um comportamento errado que foi encontrado. 🙂

Existem muitos outros recursos e ressalvas em Proxy. Leia as referências para entendê-los melhor.

Referências

⭐️ Créditos

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store