Os 3 erros mais comuns que os desenvolvedores de JavaScript fazem

POR RYAN J. PETERSON - SOFTWARE ENGINEER

Hoje, o JavaScript está no cerne de praticamente todas as aplicações web modernas. Nos últimos anos, em particular, testemunhamos a proliferação de uma ampla gama de poderosas bibliotecas e estruturas baseadas em JavaScript para o desenvolvimento de aplicativos de página única (SPA), gráficos e animações e até mesmo plataformas de JavaScript do lado do servidor. O JavaScript realmente se tornou onipresente no mundo do desenvolvimento de aplicativos web e, portanto, é uma habilidade cada vez mais importante para dominar.

Em primeiro lugar, o JavaScript pode parecer bastante simples. E de fato, criar funcionalidade básica de JavaScript em uma página da web é uma tarefa bastante intensa para qualquer desenvolvedor de software experiente. No entanto, o idioma é significativamente mais matizado, poderoso e complexo do que inicialmente seria levado a acreditar. Na verdade, muitas das sutilezas do JavaScript levam a uma série de problemas comuns que impedem que funcione.

Erro comum nº 1: referências incorretas --- this

Como as técnicas de codificação de JavaScript e os padrões de design tornaram-se cada vez mais sofisticados ao longo dos anos, houve um aumento correspondente na proliferação de escopos de auto referência dentro de call-backs[i] e fechamentos, que são uma fonte bastante comum de "this/that confusion".

Considere este exemplo de trecho de código:

Game.prototype.restart = function () {
  this.clearLocalStorage ();
  this.timer = setTimeout (function () {
    this.clearBoard (); // o que é isso (this)"?
  }, 0);
};
Executar o código acima resulta no seguinte erro:

Uncaught TypeError: indefinido não é uma função
Por quê?

É tudo sobre o contexto. A razão pela qual você obtém o erro acima é porque, quando invoca setTimeout (), você está realmente invocando window.setTimeout (). Como resultado, a função anônima passada para setTimeout () está sendo definida no contexto do objeto de window, que não possui nenhum método clearBoard ().

Uma solução tradicional, compatível com o navegador antigo, é simplesmente salvar sua referência this em uma variável que pode ser herdada pelo fechamento; por exemplo.:

Game.prototype.restart = function () {
  this.clearLocalStorage ();
  var self = this; // salve a referência a 'this', enquanto ainda é isso!
  this.timer = setTimeout (function () {
    self.clearBoard (); // oh OK, eu sei quem é o "eu"!
  }, 0);
};
Alternativamente, em navegadores mais novos, você pode usar o método bind () para passar na referência apropriada:

Game.prototype.restart = function () {
  this.clearLocalStorage ();
  this.timer = setTimeout (this.reset.bind (this), 0); // bind to 'this'
};

Game.prototype.reset = function () {
    this.clearBoard (); // ahhh, de volta ao contexto do direito 'this'!
};


Erro comum # 2: pensando que existe um escopo de nível de bloco

Uma fonte comum de confusão entre desenvolvedores de JavaScript (e, portanto, uma fonte comum de bugs) é assumir que o JavaScript cria um novo escopo para cada bloco de código. Embora isso seja verdade em muitos outros idiomas, não é verdade em JavaScript. Considere, por exemplo, o seguinte código:

for (var i = 0; i <10; i ++) {
  / * ... * /
}
console.log (i); // qual será esse resultado?
Se você adivinhar que a chamada console.log () emitiria indefinido ou lançaria um erro, você adivinhou incorretamente. Acredite ou não, ele emitirá 10. Por quê?

Na maioria dos outros idiomas, o código acima levaria a um erro porque a "vida" (ou seja, o escopo) da variável i seria restrita ao bloco for. Em JavaScript, porém, esse não é o caso e a variável i permanece no escopo mesmo após o loop for concluído, mantendo seu último valor depois de sair do loop. (Esse comportamento é conhecido, aliás, como elevação de variável).

Vale ressaltar, no entanto, que o suporte para escopos de nível de bloco está fazendo o seu caminho para o JavaScript através da nova palavra-chave Let. A palavra-chave Let está disponível no JavaScript 1.7 e está programada para se tornar uma palavra-chave JavaScript oficialmente aceita a partir do ECMAScript 6.

Erro comum # 3: Criando vazamentos de memória

Os vazamentos de memória são problemas de JavaScript quase inevitáveis se você não estiver codificando conscientemente para evitá-los. Existem inúmeras maneiras de ocorrerem, então vamos destacar algumas das suas ocorrências mais comuns.

Vazamento de memória Exemplo 1: referências de manobra para objetos desaparecidos

Considere o seguinte código:

var theThing = null;
var replaceThing = function () {
  var priorThing = theThing; // aguarde a coisa anterior
  var unused = function () {
    // 'não utilizado' é o único lugar onde 'priorThing' é referenciado,
    //, mas "não utilizado" nunca é invocado
    se (priorThing) {
      console.log ("oi");
    }
  };
  theThing = {
    longStr: nova Array (1000000) .join ('*'), // cria um objeto de 1MB
    someMethod: function () {
      console.log (someMessage);
    }
  };
};
setInterval (replaceThing, 1000); // invoca `replaceThing 'uma vez a cada segundo
Se você executar o código acima e monitorar a utilização da memória, você achará que você obteve um enorme vazamento de memória, vazando um megabyte por segundo! E mesmo um GC manual não ajuda. Então, parece que estamos vazando longStr sempre que o replaceThing é chamado. Mas por que?

Vamos examinar as coisas com mais detalhes:

Cada objeto TheThing contém seu próprio objeto LongStr de 1 MB. A cada segundo, quando chamamos o replaceThing, ele mantém uma referência ao objeto anterior do theThing no PriorThing. Mas ainda não pensamos que isso seria um problema, já que cada vez, o PriorThing anteriormente referenciado seria referenciado (quando o PriorThing é reiniciado via priorThing = theThing;). E, além disso, é apenas referenciado no corpo principal de replaceThing e na função não utilizada que, de fato, nunca foi usada.

Então, novamente ficamos nos perguntando por que há um vazamento de memória aqui!?

Para entender o que está acontecendo, precisamos entender melhor como as coisas funcionam em JavaScript sob o capô. A maneira típica em que os encerramentos são implementados é que cada objeto de função tem um link para um objeto de estilo de dicionário que representa seu escopo lexical. Se ambas as funções definidas dentro do replaceThing realmente usado anteriormente, seria importante que ambos obtivessem o mesmo objeto, mesmo que PriorThing seja atribuído uma e outra vez, de modo que ambas as funções compartilhem o mesmo ambiente lexical. Mas assim que uma variável é usada por qualquer fechamento, ela termina no ambiente lexical compartilhado por todos os encerramentos nesse escopo. E essa pequena nuance é o que leva a esse vazamento de memória gnarly. (Mais detalhes sobre isso estão disponíveis aqui.)

Folha de memória Exemplo 2: Referências circulares

Considere este fragmento de código:

função addClickHandler (elemento) {
    element.click = function onClick (e) {
        alerta ("clicou o" + element.nodeName)
    }
}
Aqui, onClick tem um fechamento que mantém uma referência ao elemento (via element.nodeName). Ao também atribuir onClique para element.click, a referência circular é criada; isto é .: elemento -> onClick -> elemento -> onClick -> elemento ...

Curiosamente, mesmo que o elemento seja removido do DOM, a auto-referência circular acima evitaria elementos e onClick de serem coletados e, portanto, um vazamento de memória.

Evitando vazamentos de memória: o que você precisa saber

O gerenciamento de memória de JavaScript (e, em pacotes, coleta de lixo) é amplamente baseado na noção de acessibilidade do objeto.

Os seguintes objetos são assumidos como acessíveis e são conhecidos como "raízes":

Objetos referenciados de qualquer lugar na pilha de chamadas atual (isto é, todas as variáveis e parâmetros locais nas funções atualmente invocadas e todas as variáveis no escopo)
Todas as variáveis globais
Os objetos são mantidos na memória pelo menos desde que sejam acessíveis a partir de qualquer uma das raízes através de uma referência, ou uma cadeia de referências.

Existe um colector de lixo (GC) no navegador que limpa a memória ocupada por objetos inacessíveis; ou seja, os objetos serão removidos da memória se e somente se o GC acreditar que eles são inacessíveis. Infelizmente, é bastante fácil acabar com objetos "zumbis" extintos que de fato não estão em uso, mas que o GC ainda pensa ser "acessível".



[i] A chamada de retorno é qualquer código executável que seja passado como um argumento para outro código, o que é esperado para chamar (executar) o argumento em um determinado momento.