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/