Desenvolvimento

12 fev, 2016

Sobre classes e funções Arrow (um conto preventivo)

Publicidade

Eis que temos novidades! A função Arrow, quando bem construída, afasta a penosa palavra-chave function e (em virtude do escopo léxico this) compra a alegria de muitos programadores JavaScript.No entanto, como relatado a seguir, mesmo as melhores ferramentas devem ser usadas com critério.

Atualizador dinâmico

Expressões de função tradicionais criam uma função cujo valor this é dinâmico e é tanto o objeto que o chamou, como o objeto¹ global quando não há nenhuma chamada explícita. Expressões de função Arrow, por outro lado, sempre assumem esse valor do código em volta.

let outerThis, tfeThis, afeThis;
let obj = {
  outer() {
    outerThis = this;
 
    traditionalFE = function() {tfeThis = this};
    traditionalFE();
 
    arrowFE = () => afeThis = this;
    arrowFE();
  }
}
obj.outer();
 
outerThis; // obj
tfeThis; // global
afeThis; // obj
outerThis === afeThis; // true

Funções e classes Arrow

Dada a abordagem sem sentido da função Arrow para contextos, é tentador usá-la como um substituto para métodos em classes. Considere esta classe simples que suprime todos os cliques dentro de um determinado container e relata o nó DOM cujo evento de clique foi suprimido:

class ClickSuppresser {
  constructor(domNode) {
    this.container = domNode;
    this.initialize();
  }
 
  suppressClick(e) {
    e.preventDefault();
    e.stopPropagation();
    this.clickSuppressed(e);
  }
 
  clickSuppressed(e) {
    console.log('click suppressed on', e.target);
  }
 
  initialize() {
    this.container.addEventListener(
      'click', this.suppressClick.bind(this));
  }
}

Essa implementação utiliza o método ES6 de sintaxe abreviada. Temos que conectar o ouvinte de evento à instância atual (linha 18); caso contrário, a o valor this em suppressClick seria o nó do container.

Usar funções Arrow no lugar da sintaxe do método elimina a necessidade de ligar o handler:

class ClickSuppresser {
  constructor(domNode) {
    this.container = domNode;
    this.initialize();
  }
 
  suppressClick = e => {
    e.preventDefault();
    e.stopPropagation();
    this.clickSuppressed(e);
  }
 
  clickSuppressed = e => {
    console.log('click suppressed on', e.target);
  }
 
  initialize = () => {
    this.container.addEventListener(
      'click', this.suppressClick);
  }
}

Perfeito!

Mas o que o que é isso?

ClickSuppresser.prototype.suppressClick; // undefined
ClickSuppresser.prototype.clickSuppressed; // undefined
ClickSuppresser.prototype.initialize; // undefined

Por que as funções não foram adicionadas ao protótipo?

Acontece que o problema não é tanto a própria função Arrow, mas como ela chega lá. Funções Arrow não são métodos, elas são expressões de função anônimas, então a única maneira de adicioná-las a uma classe é por atribuição a uma propriedade. E as classes ES lidam com métodos e propriedades de formas completamente diferentes.

Métodos são adicionados ao protótipo da classe, que é onde nós queremos que eles estejam – isso significa que eles são definidos apenas uma vez, em vez de uma vez por instância. Por outro lado, a sintaxe da propriedade classe (que, no momento da escrita, é um candidato proposto² a ES7) é apenas para atribuir as mesmas propriedades a cada instância. Com efeito, propriedades de classe funcionam assim:

class ClickSuppresser {
  constructor(domNode) {
 
    this.suppressClick = e => {...}
    this.clickSuppressed = e => {...}
    this.initialize = e => {...}
 
    this.node = domNode;
    this.initialize();
  }
}

Em outras, palavras nosso código de exemplo irá redefinir todas as três funções cada vez que uma nova instância do ClickSuppresser for criada.

const cs1 = new ClickSuppresser();
const cs2 = new ClickSuppresser();
 
cs1.suppressClick === cs2.suppressClick; // false
cs1.clickSuppressed === cs2.clickSuppressed; // false
cs1.initialize === cs2.initialize; // false

Na melhor das hipóteses, isso é surpreendente e intuitivo, e na pior é desnecessariamente ineficiente. De qualquer maneira, ele derrota o propósito de usar uma classe ou um protótipo compartilhado.

Onde (doce ironia) funções Arrow vêm nos salvar

Desanimado por essa mudança inesperada dos acontecimentos, o nosso herói será revertido para a sintaxe do método padrão. Mas ainda há a questão complicada da função bind. Além de ser relativamente lenta, bind cria um invólucro opaco que é difícil de depurar.

Ainda assim, nada de dragão invencível. Podemos substituir bind da nossa função mais cedo, com uma função Arrow.

initialize() {
  this.container.addEventListener(
    'click', e => this.suppressClick(e));
}

Por que isso funciona? Como suppressClick é definido utilizando método de sintaxe regular, ele irá adquirir o contexto do exemplo invocando (this, no exemplo acima). E uma vez que as funções Arrow possuem escopo léxico, this será a instância atual de nossa classe.

Se você não quer ter de olhar vários argumentos de cada vez, você pode tirar vantagem do operador rest/spread:

initialize() {
  this.container.addEventListener(
    'click', (...args) => this.suppressClick(...args));
}

Encerrando

Eu nunca me senti confortável com funções Arrow como substitutas para métodos de classe. Os métodos devem ter escopo dinâmicos, de acordo com a instância que os chamam, mas uma função Arrow é, por definição, estática de escopo. Como se vê, a questão de definição do âmbito do escopo é antecipada pela questão da eficiência igualmente problemática que vem usando as propriedades para descrever a funcionalidade comum. De qualquer maneira, você deve pensar duas vezes sobre o uso de uma função Arrow como parte de sua definição de classe.

Moral: Funções Arrow são complexas, mas usar a ferramenta certa para o trabalho é sempre melhor.

¹ undefined no modo estrito

² https://github.com/jeffmo/es-class-static-properties-and-fields

***

Angus Croll faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://javascriptweblog.wordpress.com/2015/11/02/of-classes-and-arrow-functions-a-cautionary-tale/