Código JavaScript com erros: os 10 erros mais comuns que os desenvolvedores de JavaScript cometem

Publicados: 2022-03-11

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

À primeira vista, JavaScript pode parecer bastante simples. E, de fato, construir funcionalidades básicas de JavaScript em uma página da Web é uma tarefa bastante simples para qualquer desenvolvedor de software experiente, mesmo que seja novo em JavaScript. No entanto, a linguagem é significativamente mais matizada, poderosa e complexa do que inicialmente se levaria a acreditar. De fato, muitas das sutilezas do JavaScript levam a uma série de problemas comuns que o impedem de funcionar – 10 dos quais discutimos aqui – que são importantes para se estar ciente e evitar na busca para se tornar um desenvolvedor mestre em JavaScript.

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

Certa vez ouvi um comediante dizer:

Eu não estou realmente aqui, porque o que está aqui, além de lá, sem o 't'?

Essa piada de muitas maneiras caracteriza o tipo de confusão que geralmente existe para os desenvolvedores em relação à palavra-chave this do JavaScript. Quero dizer, this é realmente isso, ou é algo completamente diferente? Ou é indefinido?

À medida que as técnicas de codificação JavaScript e os padrões de design se tornaram cada vez mais sofisticados ao longo dos anos, houve um aumento correspondente na proliferação de escopos de auto-referência em retornos de chamada e encerramentos, que são uma fonte bastante comum de “isso/aquela confusão”.

Considere este trecho de código de exemplo:

 Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // what is "this"? }, 0); };

A execução do código acima resulta no seguinte erro:

 Uncaught TypeError: undefined is not a function

Por quê?

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

Uma solução tradicional compatível com navegadores antigos é simplesmente salvar sua referência a this em uma variável que pode ser herdada pelo encerramento; por exemplo:

 Game.prototype.restart = function () { this.clearLocalStorage(); var self = this; // save reference to 'this', while it's still this! this.timer = setTimeout(function(){ self.clearBoard(); // oh OK, I do know who 'self' is! }, 0); };

Alternativamente, em navegadores mais novos, você pode usar o método bind() para passar a 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, back in the context of the right 'this'! };

Erro comum nº 2: pensar que há escopo em nível de bloco

Conforme discutido em nosso Guia de contratação de JavaScript, uma fonte comum de confusão entre os desenvolvedores de JavaScript (e, portanto, uma fonte comum de bugs) é supor que o JavaScript cria um novo escopo para cada bloco de código. Embora isso seja verdade em muitas outras linguagens, não é verdade em JavaScript. Considere, por exemplo, o seguinte código:

 for (var i = 0; i < 10; i++) { /* ... */ } console.log(i); // what will this output?

Se você adivinhar que a chamada console.log() produziria undefined ou geraria um erro, você adivinhou incorretamente. Acredite ou não, ele produzirá 10 . Por quê?

Na maioria das outras linguagens, o código acima levaria a um erro porque a “vida” (ou seja, escopo) da variável i estaria restrita ao bloco for . Em JavaScript, porém, este não é o caso e a variável i permanece no escopo mesmo após o loop for ter sido concluído, mantendo seu último valor após sair do loop. (Esse comportamento é conhecido, aliás, como içamento variável).

Vale a pena notar, porém, que o suporte para escopos em nível de bloco está chegando ao JavaScript por meio da nova palavra-chave let . A palavra-chave let já está disponível no JavaScript 1.7 e está programada para se tornar uma palavra-chave JavaScript oficialmente suportada a partir do ECMAScript 6.

Novo em JavaScript? Leia sobre escopos, protótipos e muito mais.

Erro comum nº 3: criando vazamentos de memória

Vazamentos de memória são problemas de JavaScript quase inevitáveis ​​se você não estiver codificando conscientemente para evitá-los. Existem várias maneiras de eles ocorrerem, então vamos destacar apenas algumas de suas ocorrências mais comuns.

Exemplo de vazamento de memória 1: referências pendentes para objetos extintos

Considere o seguinte código:

 var theThing = null; var replaceThing = function () { var priorThing = theThing; // hold on to the prior thing var unused = function () { // 'unused' is the only place where 'priorThing' is referenced, // but 'unused' never gets invoked if (priorThing) { console.log("hi"); } }; theThing = { longStr: new Array(1000000).join('*'), // create a 1MB object someMethod: function () { console.log(someMessage); } }; }; setInterval(replaceThing, 1000); // invoke `replaceThing' once every second

Se você executar o código acima e monitorar o uso de memória, descobrirá que tem um enorme vazamento de memória, vazando um megabyte completo por segundo! E mesmo um GC manual não ajuda. Então parece que estamos vazando longStr toda vez que replaceThing é chamado. Mas por que?

Vamos examinar as coisas com mais detalhes:

Cada objeto theThing contém seu próprio objeto longStr 1 MB. A cada segundo, quando chamamos replaceThing , ele mantém uma referência ao objeto theThing anterior em priorThing . Mas ainda não acharíamos que isso seria um problema, já que a cada vez, o priorThing referenciado anteriormente seria desreferenciado (quando priorThing é redefinido via priorThing = theThing; ). E, além disso, é referenciado apenas no corpo principal de replaceThing e na função unused que, de fato, nunca é usada.

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

Para entender o que está acontecendo, precisamos entender melhor como as coisas estão funcionando em JavaScript sob o capô. A maneira típica de implementação de closures é que cada objeto de função tem um link para um objeto de estilo de dicionário que representa seu escopo léxico. Se ambas as funções definidas dentro de replaceThing realmente usassem priorThing , seria importante que ambas obtivessem o mesmo objeto, mesmo que priorThing fosse atribuído repetidamente, para que ambas as funções compartilhem o mesmo ambiente léxico. Mas assim que uma variável é usada por qualquer encerramento, ela acaba no ambiente léxico compartilhado por todos os encerramentos nesse escopo. E essa pequena nuance é o que leva a esse vazamento de memória retorcido. (Mais detalhes sobre isso estão disponíveis aqui.)

Exemplo de vazamento de memória 2: referências circulares

Considere este fragmento de código:

 function addClickHandler(element) { element.click = function onClick(e) { alert("Clicked the " + element.nodeName) } }

Aqui, onClick tem um encerramento que mantém uma referência ao element (via element.nodeName ). Atribuindo também onClick a element.click , a referência circular é criada; ou seja: element -> onClick -> element -> onClick -> element

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

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

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

Os seguintes objetos são considerados alcançáveis ​​e são conhecidos como “raízes”:

  • Objetos referenciados de qualquer lugar na pilha de chamadas atual (ou seja, todas as variáveis ​​e parâmetros locais nas funções que estão sendo invocadas no momento e todas as variáveis ​​no escopo de fechamento)
  • Todas as variáveis ​​globais

Os objetos são mantidos na memória pelo menos enquanto estiverem acessíveis a partir de qualquer uma das raízes por meio de uma referência ou de uma cadeia de referências.

Existe um Garbage Collector (GC) no navegador que limpa a memória ocupada por objetos inacessíveis; isto é, os objetos serão removidos da memória se e somente se o GC acreditar que eles são inalcançáveis. Infelizmente, é bastante fácil acabar com objetos “zumbis” extintos que, na verdade, não estão mais em uso, mas que o GC ainda acha que são “alcançáveis”.

Relacionado: Práticas recomendadas e dicas de JavaScript dos desenvolvedores da Toptal

Erro comum nº 4: confusão sobre igualdade

Uma das conveniências do JavaScript é que ele forçará automaticamente qualquer valor referenciado em um contexto booleano para um valor booleano. Mas há casos em que isso pode ser tão confuso quanto conveniente. Alguns dos seguintes, por exemplo, são conhecidos por morder muitos desenvolvedores de JavaScript:

 // All of these evaluate to 'true'! console.log(false == '0'); console.log(null == undefined); console.log(" \t\r\n" == 0); console.log('' == 0); // And these do too! if ({}) // ... if ([]) // ...

Com relação aos dois últimos, apesar de vazios (o que pode levar a crer que eles seriam avaliados como false ), tanto {} quanto [] são de fato objetos e qualquer objeto será coagido a um valor booleano true em JavaScript, consistente com a especificação ECMA-262.

Como esses exemplos demonstram, as regras de coerção do tipo podem às vezes ser claras como lama. Assim, a menos que a coerção de tipo seja explicitamente desejada, normalmente é melhor usar === e !== (em vez de == e != ), para evitar quaisquer efeitos colaterais não intencionais da coerção de tipo. ( == e != executam automaticamente a conversão de tipo ao comparar duas coisas, enquanto === e !== fazem a mesma comparação sem conversão de tipo.)

E completamente como um ponto secundário – mas já que estamos falando de coerção de tipo e comparações – vale a pena mencionar que comparar NaN com qualquer coisa (mesmo NaN !) sempre retornará false . Portanto, você não pode usar os operadores de igualdade ( == , === , != , !== ) para determinar se um valor é NaN ou não. Em vez disso, use a função global isNaN() integrada:

 console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(isNaN(NaN)); // true

Erro comum nº 5: manipulação ineficiente do DOM

JavaScript torna relativamente fácil manipular o DOM (ou seja, adicionar, modificar e remover elementos), mas não faz nada para promover isso de forma eficiente.

Um exemplo comum é o código que adiciona uma série de elementos DOM, um de cada vez. Adicionar um elemento DOM é uma operação cara. O código que adiciona vários elementos DOM consecutivamente é ineficiente e provavelmente não funcionará bem.

Uma alternativa eficaz quando vários elementos DOM precisam ser adicionados é usar fragmentos de documentos, melhorando assim a eficiência e o desempenho.

Por exemplo:

 var div = document.getElementsByTagName("my_div"); var fragment = document.createDocumentFragment(); for (var e = 0; e < elems.length; e++) { // elems previously set to list of elements fragment.appendChild(elems[e]); } div.appendChild(fragment.cloneNode(true));

Além da eficiência inerentemente aprimorada dessa abordagem, criar elementos DOM anexados é caro, enquanto criá-los e modificá-los enquanto desanexados e depois anexá-los produz um desempenho muito melhor.

Erro comum nº 6: uso incorreto de definições de função dentro for loops for

Considere este código:

 var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example for (var i = 0; i < n; i++) { elements[i].onclick = function() { console.log("This is element #" + i); }; }

Com base no código acima, se houvesse 10 elementos de entrada, clicar em qualquer um deles exibiria “Este é o elemento #10”! Isso porque, no momento em que onclick for invocado para qualquer um dos elementos, o loop for acima estará concluído e o valor de i já será 10 (para todos eles).

Veja como podemos corrigir os problemas de código acima, para obter o comportamento desejado:

 var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example var makeHandler = function(num) { // outer function return function() { // inner function console.log("This is element #" + num); }; }; for (var i = 0; i < n; i++) { elements[i].onclick = makeHandler(i+1); }

Nesta versão revisada do código, makeHandler é executado imediatamente cada vez que passamos pelo loop, cada vez recebendo o valor atual de i+1 e ligando-o a uma variável num com escopo definido. A função externa retorna a função interna (que também usa essa variável num com escopo definido) e o onclick do elemento é definido para essa função interna. Isso garante que cada onclick receba e use o valor i adequado (por meio da variável num com escopo definido).

Erro comum nº 7: Falha em alavancar adequadamente a herança prototípica

Uma porcentagem surpreendentemente alta de desenvolvedores de JavaScript não consegue entender completamente e, portanto, aproveitar totalmente os recursos da herança prototípica.

Aqui está um exemplo simples. Considere este código:

 BaseObject = function(name) { if(typeof name !== "undefined") { this.name = name; } else { this.name = 'default' } };

Parece bastante simples. Se você fornecer um nome, use-o, caso contrário, defina o nome como 'padrão'; por exemplo:

 var firstObj = new BaseObject(); var secondObj = new BaseObject('unique'); console.log(firstObj.name); // -> Results in 'default' console.log(secondObj.name); // -> Results in 'unique'

Mas e se fizéssemos isso:

 delete secondObj.name;

Teríamos então:

 console.log(secondObj.name); // -> Results in 'undefined'

Mas não seria melhor que isso voltasse para 'padrão'? Isso pode ser feito facilmente, se modificarmos o código original para alavancar a herança prototípica, como segue:

 BaseObject = function (name) { if(typeof name !== "undefined") { this.name = name; } }; BaseObject.prototype.name = 'default';

Com esta versão, BaseObject herda a propriedade name de seu objeto prototype , onde é definido (por padrão) como 'default' . Assim, se o construtor for chamado sem um nome, o nome será padronizado como default . Da mesma forma, se a propriedade name for removida de uma instância de BaseObject , a cadeia de protótipos será pesquisada e a propriedade name será recuperada do objeto prototype onde seu valor ainda é 'default' . Então agora obtemos:

 var thirdObj = new BaseObject('unique'); console.log(thirdObj.name); // -> Results in 'unique' delete thirdObj.name; console.log(thirdObj.name); // -> Results in 'default'

Erro comum nº 8: criar referências incorretas a métodos de instância

Vamos definir um objeto simples e criar uma instância dele, como segue:

 var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject();

Agora, por conveniência, vamos criar uma referência ao método whoAmI , presumivelmente para que possamos acessá-lo apenas por whoAmI() em vez de obj.whoAmI() :

 var whoAmI = obj.whoAmI;

E só para ter certeza de que tudo parece copacético, vamos imprimir o valor da nossa nova variável whoAmI :

 console.log(whoAmI);

Saídas:

 function () { console.log(this === window ? "window" : "MyObj"); }

OK legal. Parece bem.

Mas agora, veja a diferença quando invocamos obj.whoAmI() vs. nossa referência de conveniência whoAmI() :

 obj.whoAmI(); // outputs "MyObj" (as expected) whoAmI(); // outputs "window" (uh-oh!)

O que deu errado?

O headfake aqui é que, quando fizemos a atribuição var whoAmI = obj.whoAmI; , a nova variável whoAmI estava sendo definida no namespace global . Como resultado, this valor é window , não a instância obj de MyObject !

Assim, se realmente precisarmos criar uma referência a um método existente de um objeto, precisamos ter certeza de fazê-lo dentro do namespace desse objeto, para preservar o valor de this . Uma maneira de fazer isso seria, por exemplo, da seguinte forma:

 var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject(); obj.w = obj.whoAmI; // still in the obj namespace obj.whoAmI(); // outputs "MyObj" (as expected) obj.w(); // outputs "MyObj" (as expected)

Erro comum nº 9: fornecer uma string como o primeiro argumento para setTimeout ou setInterval

Para começar, vamos esclarecer algo aqui: Fornecer uma string como o primeiro argumento para setTimeout ou setInterval não é um erro em si. É um código JavaScript perfeitamente legítimo. A questão aqui é mais de desempenho e eficiência. O que raramente é explicado é que, nos bastidores, se você passar uma string como o primeiro argumento para setTimeout ou setInterval , ela será passada para o construtor da função para ser convertida em uma nova função. Esse processo pode ser lento e ineficiente e raramente é necessário.

A alternativa para passar uma string como o primeiro argumento para esses métodos é passar uma função . Vamos dar uma olhada em um exemplo.

Aqui, então, seria um uso bastante típico de setInterval e setTimeout , passando uma string como o primeiro parâmetro:

 setInterval("logTime()", 1000); setTimeout("logMessage('" + msgValue + "')", 1000);

A melhor escolha seria passar uma função como argumento inicial; por exemplo:

 setInterval(logTime, 1000); // passing the logTime function to setInterval setTimeout(function() { // passing an anonymous function to setTimeout logMessage(msgValue); // (msgValue is still accessible in this scope) }, 1000);

Erro comum nº 10: Falha ao usar o “modo estrito”

Conforme explicado em nosso Guia de Contratação de JavaScript, o “modo estrito” (ou seja, incluindo 'use strict'; no início de seus arquivos de origem JavaScript) é uma maneira de impor voluntariamente análise e tratamento de erros mais rigorosos em seu código JavaScript em tempo de execução, bem como como torná-lo mais seguro.

Embora, reconhecidamente, deixar de usar o modo estrito não seja um “erro” em si, seu uso é cada vez mais incentivado e sua omissão é cada vez mais considerada uma má forma.

Aqui estão alguns dos principais benefícios do modo estrito:

  • Facilita a depuração. Erros de código que de outra forma seriam ignorados ou teriam falhado silenciosamente agora gerarão erros ou lançarão exceções, alertando você mais cedo sobre problemas em seu código e direcionando você mais rapidamente para a fonte.
  • Evita globais acidentais. Sem o modo estrito, atribuir um valor a uma variável não declarada cria automaticamente uma variável global com esse nome. Este é um dos erros mais comuns em JavaScript. No modo estrito, tentar fazer isso gera um erro.
  • Elimina this coerção . Sem o modo estrito, uma referência a this valor nulo ou indefinido é automaticamente forçada para o global. Isso pode causar muitos headfakes e bugs do tipo arrancar o cabelo. No modo estrito, fazer referência a this valor nulo ou indefinido gera um erro.
  • Não permite nomes de propriedade ou valores de parâmetro duplicados. O modo estrito gera um erro quando detecta uma propriedade nomeada duplicada em um objeto (por exemplo, var object = {foo: "bar", foo: "baz"}; ) ou um argumento nomeado duplicado para uma função (por exemplo, function foo(val1, val2, val1){} ), capturando assim o que é quase certamente um bug em seu código que você poderia ter perdido muito tempo rastreando.
  • Torna eval() mais segura. Existem algumas diferenças na forma como eval() se comporta no modo estrito e no modo não estrito. Mais significativamente, no modo estrito, variáveis ​​e funções declaradas dentro de uma instrução eval() não são criadas no escopo de contenção (elas são criadas no escopo de contenção no modo não estrito, que também pode ser uma fonte comum de problemas).
  • Lança erro no uso inválido de delete . O operador delete (usado para remover propriedades de objetos) não pode ser usado em propriedades não configuráveis ​​do objeto. O código não estrito falhará silenciosamente quando for feita uma tentativa de excluir uma propriedade não configurável, enquanto o modo estrito gerará um erro nesse caso.

Embrulhar

Como acontece com qualquer tecnologia, quanto melhor você entender por que e como o JavaScript funciona e não funciona, mais sólido será seu código e mais você poderá aproveitar efetivamente o verdadeiro poder da linguagem. Por outro lado, a falta de compreensão adequada dos paradigmas e conceitos de JavaScript é, de fato, onde residem muitos problemas de JavaScript.

Familiarizar-se completamente com as nuances e sutilezas do idioma é a estratégia mais eficaz para melhorar sua proficiência e aumentar sua produtividade. Evitar muitos erros comuns de JavaScript ajudará quando seu JavaScript não estiver funcionando.

Relacionado: Promessas de JavaScript: um tutorial com exemplos