Código JavaScript con errores: los 10 errores más comunes que cometen los desarrolladores de JavaScript

Publicado: 2022-03-11

Hoy en día, JavaScript es el núcleo de prácticamente todas las aplicaciones web modernas. Los últimos años en particular han sido testigos de la proliferación de una amplia gama de potentes bibliotecas y marcos basados ​​en JavaScript para el desarrollo, gráficos y animaciones de aplicaciones de una sola página (SPA), e incluso plataformas JavaScript del lado del servidor. JavaScript se ha vuelto realmente omnipresente en el mundo del desarrollo de aplicaciones web y, por lo tanto, es una habilidad cada vez más importante de dominar.

A primera vista, JavaScript puede parecer bastante simple. Y, de hecho, incorporar la funcionalidad básica de JavaScript en una página web es una tarea bastante sencilla para cualquier desarrollador de software experimentado, incluso si es nuevo en JavaScript. Sin embargo, el lenguaje es significativamente más matizado, poderoso y complejo de lo que uno podría creer inicialmente. De hecho, muchas de las sutilezas de JavaScript conducen a una serie de problemas comunes que impiden que funcione (10 de los cuales discutimos aquí) que es importante tener en cuenta y evitar en la búsqueda de convertirse en un desarrollador maestro de JavaScript.

Error común #1: Referencias incorrectas a this

Una vez escuché a un comediante decir:

No estoy realmente aquí, porque ¿qué hay aquí, además de allá, sin la 't'?

Esa broma en muchos sentidos caracteriza el tipo de confusión que a menudo existe para los desarrolladores con respecto a this palabra clave de JavaScript. Quiero decir, ¿es this realmente esto, o es algo completamente diferente? ¿O es indefinido?

A medida que las técnicas de codificación y los patrones de diseño de JavaScript se han vuelto cada vez más sofisticados a lo largo de los años, ha habido un aumento correspondiente en la proliferación de ámbitos autorreferenciales dentro de las devoluciones de llamada y los cierres, que son una fuente bastante común de "esta/esa confusión".

Considere este fragmento de código de ejemplo:

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

Ejecutar el código anterior da como resultado el siguiente error:

 Uncaught TypeError: undefined is not a function

¿Por qué?

Se trata de contexto. La razón por la que obtiene el error anterior es porque, cuando invoca setTimeout() , en realidad está invocando window.setTimeout() . Como resultado, la función anónima que se pasa a setTimeout() se define en el contexto del objeto window , que no tiene el método clearBoard() .

Una solución tradicional compatible con navegadores antiguos es simplemente guardar su referencia a this en una variable que luego puede ser heredada por el cierre; p.ej:

 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, en los navegadores más nuevos, puede usar el método bind() para pasar la referencia adecuada:

 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'! };

Error común n.º 2: pensar que hay un alcance a nivel de bloque

Como se discutió en nuestra Guía de contratación de JavaScript, una fuente común de confusión entre los desarrolladores de JavaScript (y, por lo tanto, una fuente común de errores) es asumir que JavaScript crea un nuevo alcance para cada bloque de código. Aunque esto es cierto en muchos otros idiomas, no lo es en JavaScript. Considere, por ejemplo, el siguiente código:

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

Si supone que la llamada a console.log() generaría un resultado undefined o arrojaría un error, lo supuso incorrectamente. Lo crea o no, generará 10 . ¿Por qué?

En la mayoría de los demás lenguajes, el código anterior daría lugar a un error porque la "vida" (es decir, el alcance) de la variable i estaría restringida al bloque for . Sin embargo, en JavaScript, este no es el caso y la variable i permanece en el alcance incluso después de que se haya completado el bucle for , conservando su último valor después de salir del bucle. (Este comportamiento se conoce, por cierto, como izaje variable).

Sin embargo, vale la pena señalar que la compatibilidad con los ámbitos de nivel de bloque se está abriendo camino en JavaScript a través de la palabra clave new let . La palabra clave let ya está disponible en JavaScript 1.7 y está programada para convertirse en una palabra clave de JavaScript admitida oficialmente a partir de ECMAScript 6.

¿Nuevo en JavaScript? Infórmese sobre alcances, prototipos y más.

Error común #3: Crear fugas de memoria

Las fugas de memoria son problemas de JavaScript casi inevitables si no está codificando conscientemente para evitarlos. Existen numerosas formas de que ocurran, por lo que solo destacaremos un par de sus ocurrencias más comunes.

Ejemplo de fuga de memoria 1: referencias colgantes a objetos difuntos

Considere el siguiente 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

Si ejecuta el código anterior y supervisa el uso de la memoria, descubrirá que tiene una pérdida de memoria masiva, ¡perdiendo un megabyte completo por segundo! E incluso un GC manual no ayuda. Así que parece que estamos filtrando longStr cada vez que se llama a replaceThing . ¿Pero por qué?

Examinemos las cosas con más detalle:

Cada objeto theThing contiene su propio objeto longStr 1 MB. Cada segundo, cuando llamamos replaceThing , se aferra a una referencia al objeto anterior theThing en priorThing . Pero todavía no pensaríamos que esto sería un problema, ya que cada vez que se pasa, la priorThing anterior referenciada anteriormente se eliminaría (cuando priorThing se restablece a través priorThing = theThing; ). Y además, solo se hace referencia en el cuerpo principal de replaceThing y en la función unused que, de hecho, nunca se usa.

Entonces, nuevamente nos preguntamos por qué hay una pérdida de memoria aquí.

Para entender lo que está pasando, necesitamos entender mejor cómo funcionan las cosas en JavaScript bajo el capó. La forma típica en que se implementan los cierres es que cada objeto de función tiene un enlace a un objeto de estilo diccionario que representa su alcance léxico. Si ambas funciones definidas dentro replaceThing realmente usaron priorThing , sería importante que ambas obtuvieran el mismo objeto, incluso si priorThing se asigna una y otra vez, para que ambas funciones compartan el mismo entorno léxico. Pero tan pronto como una variable es utilizada por cualquier cierre, termina en el entorno léxico compartido por todos los cierres en ese ámbito. Y ese pequeño matiz es lo que conduce a esta fuga de memoria retorcida. (Más detalles sobre esto están disponibles aquí).

Fuga de memoria Ejemplo 2: Referencias circulares

Considere este fragmento de código:

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

Aquí, onClick tiene un cierre que mantiene una referencia al element (a través element.nodeName ). Al asignar también onClick a element.click , se crea la referencia circular; es decir: element -> onClick -> element -> onClick -> element

Curiosamente, incluso si el element se elimina del DOM, la autorreferencia circular anterior evitaría que se recopile el element y onClick y, por lo tanto, una pérdida de memoria.

Evitar fugas de memoria: lo que necesita saber

La gestión de la memoria de JavaScript (y, en particular, la recolección de elementos no utilizados) se basa en gran medida en la noción de accesibilidad de los objetos.

Se supone que los siguientes objetos son accesibles y se conocen como "raíces":

  • Objetos a los que se hace referencia desde cualquier parte de la pila de llamadas actual (es decir, todas las variables y parámetros locales en las funciones que se están invocando actualmente y todas las variables en el ámbito de cierre)
  • Todas las variables globales

Los objetos se mantienen en la memoria al menos mientras sean accesibles desde cualquiera de las raíces a través de una referencia o una cadena de referencias.

Hay un Recolector de basura (GC) en el navegador que limpia la memoria ocupada por objetos inalcanzables; es decir, los objetos se eliminarán de la memoria si y solo si el GC cree que no se pueden alcanzar. Desafortunadamente, es bastante fácil terminar con objetos "zombie" obsoletos que de hecho ya no están en uso pero que el GC todavía piensa que son "accesibles".

Relacionado: Prácticas recomendadas de JavaScript y consejos de los desarrolladores de Toptal

Error común #4: Confusión sobre la igualdad

Una de las ventajas de JavaScript es que forzará automáticamente cualquier valor al que se haga referencia en un contexto booleano a un valor booleano. Pero hay casos en los que esto puede resultar tan confuso como conveniente. Se sabe que algunos de los siguientes, por ejemplo, molestan a muchos desarrolladores 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 ([]) // ...

Con respecto a los dos últimos, a pesar de estar vacíos (lo que podría llevar a creer que se evaluarían como false ), tanto {} como [] son ​​de hecho objetos y cualquier objeto será forzado a un valor booleano de true en JavaScript, consistente con la especificación ECMA-262.

Como demuestran estos ejemplos, las reglas de la coerción de tipo a veces pueden ser tan claras como el barro. En consecuencia, a menos que se desee explícitamente la coerción de tipos, normalmente es mejor usar === y !== (en lugar de == y != ), para evitar efectos secundarios no deseados de la coerción de tipos. ( == y != realizan automáticamente la conversión de tipo al comparar dos cosas, mientras que === y !== hacen la misma comparación sin conversión de tipo).

Y completamente como un punto secundario, pero dado que estamos hablando de coerción de tipos y comparaciones, vale la pena mencionar que comparar NaN con cualquier cosa (¡incluso NaN !) Siempre devolverá false . Por lo tanto, no puede utilizar los operadores de igualdad ( == , === , != , !== ) para determinar si un valor es NaN o no. En su lugar, utilice la función integrada global isNaN() :

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

Error común n.° 5: manipulación ineficiente del DOM

JavaScript hace que sea relativamente fácil manipular el DOM (es decir, agregar, modificar y eliminar elementos), pero no hace nada para promover hacerlo de manera eficiente.

Un ejemplo común es el código que agrega una serie de elementos DOM uno a la vez. Agregar un elemento DOM es una operación costosa. El código que agrega varios elementos DOM consecutivamente es ineficiente y probablemente no funcione bien.

Una alternativa eficaz cuando es necesario agregar varios elementos DOM es utilizar fragmentos de documentos en su lugar, lo que mejora tanto la eficiencia como el rendimiento.

Por ejemplo:

 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));

Además de la eficiencia inherentemente mejorada de este enfoque, la creación de elementos DOM adjuntos es costosa, mientras que crearlos y modificarlos mientras están separados y luego adjuntarlos produce un rendimiento mucho mejor.

Error común n.º 6: uso incorrecto de definiciones de funciones dentro for bucles 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); }; }

Según el código anterior, si hubiera 10 elementos de entrada, al hacer clic en cualquiera de ellos se mostraría "Este es el elemento #10". Esto se debe a que, en el momento en que se invoque onclick para cualquiera de los elementos, el bucle for anterior se habrá completado y el valor de i ya será 10 (para todos ellos).

Sin embargo, así es como podemos corregir los problemas de código anteriores para lograr el comportamiento deseado:

 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); }

En esta versión revisada del código, makeHandler se ejecuta inmediatamente cada vez que pasamos por el bucle, cada vez que recibe el valor actual de i+1 y lo vincula a una variable num con ámbito. La función externa devuelve la función interna (que también usa esta variable num con ámbito) y el onclick del elemento se establece en esa función interna. Esto asegura que cada onclick reciba y use el valor i adecuado (a través de la variable num con alcance).

Error común n.º 7: no aprovechar adecuadamente la herencia prototípica

Un porcentaje sorprendentemente alto de desarrolladores de JavaScript no comprende completamente y, por lo tanto, no aprovecha al máximo las características de la herencia de prototipos.

Aquí hay un ejemplo simple. Considere este código:

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

Parece bastante sencillo. Si proporciona un nombre, utilícelo; de lo contrario, establezca el nombre en 'predeterminado'; p.ej:

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

Pero, ¿y si hiciéramos esto?

 delete secondObj.name;

Entonces obtendríamos:

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

Pero, ¿no sería mejor que esto volviera a 'predeterminado'? Esto se puede hacer fácilmente, si modificamos el código original para aprovechar la herencia prototípica, de la siguiente manera:

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

Con esta versión, BaseObject hereda la propiedad de name de su objeto prototype , donde se establece (de forma predeterminada) en 'default' . Por lo tanto, si se llama al constructor sin un nombre, el nombre será default . Y de manera similar, si la propiedad de name se elimina de una instancia de BaseObject , se buscará en la cadena de prototipos y se recuperará la propiedad de name del objeto prototype donde su valor sigue siendo 'default' . Así que ahora obtenemos:

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

Error común n.º 8: crear referencias incorrectas a métodos de instancia

Definamos un objeto simple y creemos una instancia de él, de la siguiente manera:

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

Ahora, por conveniencia, creemos una referencia al método whoAmI , presumiblemente para que podamos acceder a él simplemente mediante whoAmI() en lugar del obj.whoAmI() () más largo:

 var whoAmI = obj.whoAmI;

Y solo para asegurarnos de que todo se vea copacetic, imprimamos el valor de nuestra nueva variable whoAmI :

 console.log(whoAmI);

Salidas:

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

Está bien. Se ve bien.

Pero ahora, observe la diferencia cuando invocamos obj.whoAmI() frente a nuestra referencia de conveniencia whoAmI() :

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

¿Qué salió mal?

El engaño aquí es que, cuando hicimos la asignación var whoAmI = obj.whoAmI; , la nueva variable whoAmI se estaba definiendo en el espacio de nombres global . Como resultado, su valor de this es window , no la instancia obj de MyObject .

Por lo tanto, si realmente necesitamos crear una referencia a un método existente de un objeto, debemos asegurarnos de hacerlo dentro del espacio de nombres de ese objeto, para preservar el valor de this . Una forma de hacerlo sería, por ejemplo, de la siguiente manera:

 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)

Error común n.º 9: proporcionar una cadena como primer argumento para setTimeout o setInterval

Para empezar, aclaremos algo aquí: Proporcionar una cadena como primer argumento para setTimeout o setInterval no es en sí mismo un error. Es un código JavaScript perfectamente legítimo. El problema aquí es más uno de rendimiento y eficiencia. Lo que rara vez se explica es que, bajo el capó, si pasa una cadena como primer argumento a setTimeout o setInterval , se pasará al constructor de funciones para convertirla en una nueva función. Este proceso puede ser lento e ineficiente y rara vez es necesario.

La alternativa a pasar una cadena como primer argumento de estos métodos es pasar una función . Echemos un vistazo a un ejemplo.

Aquí, entonces, sería un uso bastante típico de setInterval y setTimeout , pasando una cadena como primer parámetro:

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

La mejor opción sería pasar una función como argumento inicial; p.ej:

 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);

Error común n.º 10: no utilizar el "modo estricto"

Como se explica en nuestra Guía de contratación de JavaScript, el "modo estricto" (es decir, que incluye 'use strict'; al comienzo de sus archivos fuente de JavaScript) es una forma de aplicar voluntariamente un análisis más estricto y el manejo de errores en su código JavaScript en tiempo de ejecución, así como como hacerlo más seguro.

Si bien es cierto que no usar el modo estricto no es un "error" per se, su uso se fomenta cada vez más y su omisión se considera cada vez más como una mala forma.

Estos son algunos de los beneficios clave del modo estricto:

  • Facilita la depuración. Los errores de código que de otro modo se habrían ignorado o habrían fallado silenciosamente ahora generarán errores o generarán excepciones, lo que lo alertará antes sobre problemas en su código y lo dirigirá más rápidamente a su fuente.
  • Previene globales accidentales. Sin el modo estricto, asignar un valor a una variable no declarada crea automáticamente una variable global con ese nombre. Este es uno de los errores más comunes en JavaScript. En modo estricto, intentar hacerlo genera un error.
  • Elimina this coacción . Sin el modo estricto, una referencia a this valor de nulo o indefinido se convierte automáticamente en global. Esto puede causar muchas falsificaciones y errores que te arrancan el pelo. En modo estricto, hacer referencia a this valor nulo o indefinido genera un error.
  • No permite nombres de propiedades o valores de parámetros duplicados. El modo estricto arroja un error cuando detecta una propiedad con nombre duplicado en un objeto (p. ej., var object = {foo: "bar", foo: "baz"}; ) o un argumento con nombre duplicado para una función (p. ej., function foo(val1, val2, val1){} ), detectando así lo que es casi seguro un error en su código que, de otro modo, podría haber perdido mucho tiempo rastreando.
  • Hace que eval() sea más seguro. Existen algunas diferencias en la forma en que eval() se comporta en modo estricto y en modo no estricto. Lo más significativo es que, en modo estricto, las variables y funciones declaradas dentro de una instrucción eval() no se crean en el ámbito contenedor ( se crean en el ámbito contenedor en modo no estricto, lo que también puede ser una fuente común de problemas).
  • Lanza un error en el uso no válido de delete . El operador de delete (usado para eliminar propiedades de los objetos) no se puede usar en propiedades no configurables del objeto. El código no estricto fallará silenciosamente cuando se intente eliminar una propiedad no configurable, mientras que el modo estricto arrojará un error en tal caso.

Envolver

Como ocurre con cualquier tecnología, cuanto mejor comprenda por qué y cómo JavaScript funciona y no funciona, más sólido será su código y más podrá aprovechar de manera efectiva el verdadero poder del lenguaje. Por el contrario, la falta de una comprensión adecuada de los paradigmas y conceptos de JavaScript es, de hecho, donde se encuentran muchos problemas de JavaScript.

Familiarizarse a fondo con los matices y sutilezas del idioma es la estrategia más efectiva para mejorar su competencia y aumentar su productividad. Evitar muchos errores comunes de JavaScript ayudará cuando su JavaScript no funcione.

Relacionado: Promesas de JavaScript: un tutorial con ejemplos