ECMAScript 6 Proxy

ECMAScript 6 Proxy

O termo proxy tem suas origens no Direito dos países de língua inglesa. Um proxy é alguém com poderes legais de representar uma outra pessoa. No Brasil seria algo como alguém que possui uma procuração para representar uma outra. Por analogia, o termo acabou sendo usado na computação para designar softwares que atuam por outros como, por exemplo, um Web Proxy, que funciona como um intermediário para requisições de um cliente a um servidor externo.

No ECMAScript 6, um proxy é um objeto que representa um outro. Ele é capaz de interceptar chamadas às propriedades do objeto alvo, podendo até mesmo alterar o resultado da chamada.

Show me the code

Para melhor entender, vamos utilizar um exemplo simples.

function Pessoa(nome, idade) {
  this.nome = nome
  this.idade = idade
}

var joao = new Pessoa("João", 37)
console.log(joao.idade) //=> 37

Acima criamos uma função construtora Pessoa e logo após instanciamos um objeto do tipo Pessoa. Checamos sua idade com console.log e temos o resultado esperado: 37.

Agora vamos usar um Proxy para interceptar chamadas à propriedade idade do objeto joao:

proxy = new Proxy(joao, {
  get: function(target, prop) {
    if (prop === "idade") {
      console.log("Acesso a prop. idade interceptado")
    }

    return target[prop]
  }
})

Vamos passo-a-passo no código acima. O construtor de um proxy aceita dois argumentos: o primeiro é o target — o objeto que queremos interceptar as chamadas — o segundo é um objeto, que chamaremos por handler, que define o comportamento do proxy.

As chaves do objeto handler são chamadas de traps — ou armadilhas — com os valores sendo funções que definem como o proxy irá se comportar quando esta trap for disparada.

Acima usamos apenas a trapget, que é disparada quando tentamos ler uma propriedade do target — o objeto que está sendo interceptado. Vamos discutir mais a fundo sobre todas as traps logo.

Também é possível utilizar o proxy não apenas como intermediário, mas também como única porta de entrada para o objeto alvo, de forma transparente. Para isso, basta armazenar na variável que faz referência ao objeto alvo a própria referência ao proxy.

console.log(joao.idade) //=> 37

joao = proxy
console.log(joao.idade) //=> Acesso a prop. idade interceptado
                        //=> 37

Sendo assim, todo código que fazia referência à variável joao passará agora pelo proxy e este irá interceptar as chamadas ao objeto sem que seja alterado nenhum código antigo.

Aplicações

Por ser uma API muito nova, a comunidade ainda há de criar os melhores cases de uso dessa nova feature do JavaScript. Porém, alguns cenários já se mostram ideais para a aplicação dos proxies.

Validação

Podemos usar a armadilha set para validarmos o novo valor de uma propriedade do objeto alvo. Caso seja um valor inválido, disparamos um erro e o valor não é alterado.

joao = new Proxy(joao, {
  set: function(target, prop, val) {
    // Verifica se estamos acessando a propriedade `idade` do obj. alvo
    if (prop === "idade") {
      val = parseInt(val, 10)

      // Verifica se o novo valor é um inteiro
      if (isNaN(val) || !isFinite(val)) {
        throw new TypeError("Idade deve ser um inteiro")
      }

      // Verifica se o novo valor é igual ou maior que 0
      if (val < 0) {
        throw new RangeError("Idade deve ser igual ou maior que 0")
      }

      // Verifica se o novo valor é menor que 200
      if (val >= 200) {
        throw new RangeError("Idade deve ser menor que 200")
      }

      // Comportamento padrão para armazenar o novo valor
      target[prop] = val
    }
  }
})

joao.idade = "Uma string" // Irá disparar o erro `TypeError`

Neste exemplo estamos validando se a idade é:

  • Um número inteiro
  • Maior ou igual a zero
  • Menor que 200

Perceba que a validação ocorre de forma transparente. Não é usado nenhum método setIdade ou outro método que funciona de intermediário de forma explícita.

Log

Com um proxy fica fácil criar uma função que recebe um objeto, intercepta e loga todos os acessos às suas propriedades.

function loggable(target) {
  target =  new Proxy(target, {
    // Armadilha de acesso a propriedades
    get: function(target, prop) {
      console.log('Lendo prop. ' + prop)
      return target[prop]
    },

    // Armadilha de modificação de propriedades
    set: function(target, prop, val) {
      console.log('Mudando valor da prop. ' + prop + ' para ' + val)
      target[prop] = val
    },

    // Armadilha de deleção de propriedades
    deleteProperty: function(target, prop) {
      console.log('Deletando prop. ' + prop)
      delete target[prop]
    }
  })
}

var joao = new Pessoa("João", 37)
// Loga o acesso a todas as propriedades do objeto `joao`
loggable(joao)

joao.idade //=> Lendo prop. idade
joao.idade = 38 //=> Mudando valor da prop. idade para 38. Era 37
delete joao.idade //=> Deletada prop. idade

No exemplo acima criamos uma função loggable que recebe um objeto como argumento e retorna um proxy que imprime na saída padrão todo o acesso, mudança e deleção de propriedades do objeto interceptado.

Armadilhas

Abaixo, uma lista com algumas das armadilhas que achei mais úteis. Vale lembrar que existem outras além das que estão listadas abaixo. Uma documentação atualizada e completa sobre armadilhas nos proxies pode ser vista na Mozilla Developer Network.

Get

Disparada quando se tenta acessar uma propriedade no objeto alvo. A assinatura do handler é a seguinte: get function(target, name, receiver) -> any.

var foo = {bar: 1}

foo = new Proxy(foo, {
  get: function(target, name, receiver) {
    // Seu código aqui...

    // Comportamento padrão de acesso a propriedade
    return target[name]
  }
})

foo.bar // Armadilha `get` será disparada nesta linha

Set

Disparada quando se tenta trocar o valor de uma propriedade do objeto alvo. A assinatura do handler é: set function(target, name, val, receiver) -> boolean.

var foo = {}

foo = new Proxy(foo, {
  set: function(target, name, val, receiver) {
    // Seu código aqui...

    // Comportamento padrão
    return target[name] = val
  }
})

Has

Disparado quando é verificado se uma propriedade existe no objeto alvo através do código prop in proxy. A assinatura do handler é: has function(target, name) -> boolean.

var foo = {bar: 1}

foo = new Proxy(foo, {
  has: function(target, name) {
    // Comportamento padrão
    return name in target
  }
})

Enumerate

Retorna um array de string com o nomes das propriedades que devem ser lidas em um loopfor in. A assinatura do handler: enumerate function(target) -> [String].

var foo = {lorem: 1, ipsum: 2}

foo = new Proxy(foo, {
  enumerate: funtion(target) {
    return ['lorem']
  }
})

for (prop in foo) {
  // irá iterar apenas na propriedade `lorem`
}

Suporte

Proxy*--32----
* implementado, porém na antiga especificação

Agora as más notícias. Atualmente a última versão da Proxy API definida no ECMAScript 6 só está disponível no Firefox. No V8, a máquina virtual de JavaScript do Chrome e Node.js, ela também está presente, porém está implementada conforme a definição antiga. Entretanto é possível usar um shim, como o harmony proxy, para normalizar a antiga API com a nova no Chrome e Node.js.

Para mais informações sobre compatibilidade do ECMAScript 6 com os mais populares ambientes JavaScripts, aconselho uma visita na tabela de compatibilidade do kangax.

#68