Declaración y visibilidad de las variables en Javascript

Antes de comenzar a desarrollar este tema, creo que debemos tener en cuenta algo fundamental y que (por sobreentendido) creo que no le damos toda la importancia que tiene: Javascript es un lenguaje que se ejecuta dentro de un navegador.

Como lenguaje de programación, tiene una sintaxis definida. Pero como su entorno natural de ejecución para el que se concibió es para ejecutarse dentro de un navegador (y supongo que teniendo en mente que el usuario objetivo no tenía demasiados conocimientos de programación), cuando el navegador ejecuta los programas Javascript permite cierta relajación acerca de las normas cómo se debe escribir un programa. Esta relajación (supongo, como digo, que pensada para facilitar la labor de programación a no programadores) pues ha ocasionado un entorno de programación que tiene sus propias peculiaridades. Además, cada navegador es un mundo en cuanto a sus pequeñas diferencias (más aún, con el paso de los años, las versiones de Javascript han ido variando).

No soy un experto en la historia y evolución del lenguaje Javascript (ni tengo la intención de serlo). Para mí, Javascript es un lenguaje como tantos otros que he manejado (bueno, reconozco que le he cogido aprecio) que me permite programar interfases dinámicos en el navegador. Eso sí, lo que sí me esfuerzo es en conocer mis herramientas al máximo posible para que mi trabajo sea lo más eficiente posible y conseguir el nivel de calidad esperado (que se me cuele un error y le llegue al usuario final me parece inaceptable… pero desgraciadamente asumo que es un riesgo inevitable pero que intento minimizar).

Lo que percibo es que las características de relajación con las que diseñaron Javascript para facilitar su uso, con el paso del tiempo se han vuelto en su contra debido a que cada vez exigimos aplicaciones Javascript más complejas (que son incompatibles con estas normas tan relajadas).

(Vuelvo a insistir que son opiniones personales, absolutamente subjetivas y que no están apoyadas por ningún conocimiento histórico exacto de la evolución del lenguaje).

Declaración de variables

Vamos a comenzar con un trozo de código realmente simple (pero ya veréis cuánto juego nos puede dar):

Este trozo de código lo podemos ejecutar sin que se produzca ningún error (al menos, aparententemente):

  • Hemos inicializado una variable llamada myVar1 con el valor «hello».
  • A continuación, queremos modificar el valor de esta variable… pero a la hora de teclearla nos hemos equivocado (hemos tecleado myvar1, Javascript es sensible a mayúsculas y minúsculas). El problema, es que el navegador es muy tolerante con nosotros 🙂 y sobreentiende que queremos una nueva variable llamada myvar1 y nos acepta esa sentencia (y crea la nueva variable) sin ponernos ninguna pega.
  • Por último, hemos mostrado el valor de la variable myVar1 en la consola de depuración. Pero el resultado mostrado es «hello» (en lugar del valor esperado, «godbye»).

Como veis, programar en Javascript es muy fácil. Prácticamente, es «sentarte y comenzar a escribir». Cualquiera con un mínimo de sentido común puede comenzar a escribir programas en Javascript (aunque es posible que termine escribiendo código espagueti imposible de mantener). Pero, como vemos, esta facilidad tiene riesgos como éste: si nos equivocamos escribiendo el nombre de una variable, el código se ejecuta pero el resultado no es el esperado.

Para evitar este tipo de situaciones, yo suelo seguir dos «buenas prácticas» aconsejadas:

  • La primera, es forzar que mis programas Javascript sigan el llamado “modo estricto”.
  • La segunda, es que analizo mis programas Javascript con JSLint.

El «modo estricto» de Javascript

¿En qué consiste el «modo estricto» de Javascript? Consiste en una directiva que cuando la escribes en tu código Javascript fuerzas al navegador a realizar chequeos adicionales. Entre estos chequeos, está que ninguna variable puede ser usada sin antes haber sido declarada. Activemos el «modo estricto» en nuestro código anterior:

Como veis, el simple hecho de escribir al comienzo de nuestro código 'use strict'; es suficiente para activar el modo estricto (activar el modo estricto admite unas cuantas variables, pero de momento me limito a activarlo de forma global para todo mi código, sin entrar en más detalles). Y, con el «modo estricto» activado, cuando la ejecución llega a mi línea de código myvar1 = 'goodbye' la ejecución del código se para y se produce un error (y nos da la oportunidad de corregir este error).

Testear nuestros programas Javascript con la herramienta JSLint

El problema de la solución anterior es que hace falta ejecutar el código mal escrito para darnos cuenta del error («dichosos lenguajes interpretados…»). Y si el código es extenso y con muchas variantes de ejecución, cubrir todas las variantes y comprobar que no hemos cometido errores puede ser complicado. Por eso, una herramienta que considero imprescindible es la mencionada JSLint ¿Qué es lo que hace esta herramienta? Analiza nuestro código y comprueba si hemos cometido una serie de errores comunes y si seguimos una serie de «buenas prácticas» de programación.

En la práctica, mi proceso de programación no consiste en ejecutar JSLint manualmente cada vez que modifico un fichero. Lo que tengo preparado es un entorno de programación con un gestor de tareas llamado Grunt. Dicho gestor está configurado para ejecutarse en background de modo que, cada vez que modifico uno de mis ficheros Javascript:

  1. Pasa este fichero javascript por la herramienta JSLint.
  2. Si tengo definidas pruebas unitarias, las ejecuta para comprobar si mi nueva versión del programa cumple dichas pruebas.
  3. Si el análisis tiene éxito y pasa las pruebas, refresca el navegador.

De este modo, si llega a este punto, puedo observar en mi navegador la nueva versión del código que sé que (al menos) ha pasado las pruebas que me garantizan que tengo delante algo (más o menos) decente para continuar el desarrollo.

De nuevo, vuelvo a contaros algo de forma absolutamente superficial. Pero hablar sobre Grunt o sobre pruebas unitarias necesitarían sus propias entradas. Pero necesitaba mencionar, al menos, los mecanismos que disponemos para ayudarnos a garantizar que nuestro código Javascript está libre de errores.

Ya os advertí que estas pocas líneas de código nos iban a dar mucho juego para charlar un rato 🙂

Variables globales y locales

En Javascript, el alcance de las variables puede ser sólo de dos tipos:

  • Alcance global: las variables pueden ser accesible desde cualquier lugar del código, existen desde que son definidas (en «modo estricto») o son usadas por primera vez (en «modo no estricto»).
  • Alcance local: son variables declaradas dentro de una función, existirán durante el período de ejecución de la función y son accesibles exclusivamente desde dentro de la función donde son declaradas.

Pero hasta esta organización tan simple puede tener sus complejidades (que afectan a la depuración de los programas):

¿Qué es lo que he provocado en este ejemplo?:

  • De la línea 1 a la 6 defino una función hello donde defino dos variables a y b (con una pequeña diferencia: la variable a la declaro «propiamente» mientras que la variable b me limito a asignarle un valor). Nada más definirlas, compruebo si están definidas.
  • En las líneas 7 y 8 compruebo si las variables a y b están definidas (efectivamente, ninguna de las dos está definida).
  • En la línea 9, ejecuto la función hello (y me muestra la traza de que ambas variables están definidas cuando se ejecuta la función).
  • A continuación, en la línea 10, compruebo si la variable a está definida. Como era de esperar, no está definida (esta variable, «murió» cuando se terminó la ejecución de la función hello).
  • Por último, en la línea 11, compruebo si está definida la variable b. Y, para mi sorpresa, sí que está definida.

¿Qué es lo que ha ocurrido? Al definir la variable b en la línea 3 sin usar la sintaxis var, el intérprete de Javascript ha supuesto que es una variable global (que podía ya existir o puede ser una nueva variable). Así que, sin ser nuestra intención, hemos creado una nueva variable global b que existirá durante el resto de la ejecución de nuestro programa (y si ya existía una variable llamada b, habremos machacado su valor original).

Este error, lo hubiéramos evitado fácilmente si hubiéramos usado el «modo estricto».

Vamos a ver otro pequeño ejemplo, parecido al anterior:

La únicas diferencias son:

  • Hemos activado el «modo estricto».
  • Cuando hemos declarado la variable b, la he escrito window.b

Si lo ejecutamos, comprobaremos que:

  • El «modo estricto» acepta la nueva declaración de la variable b.
  • Cuando en las líneas 12 y 13 accedo a la variable b (esta vez, sin anteponerle window) puedo acceder a ella sin problemas.

¿Qué intento mostrar con este ejemplo? Intento mostrar que las variables globales que declare, el intérprete de Javascript las crea automáticamente como propiedades del objeto window del navegador. Así pues, tengamos en cuenta que las variables globales son muy cómodas de gestionar para guardar el estado de nuestros programas Javascript. Pero cuando las definimos estamos registrándolas sobre el objeto window (como si fuera un cuarto trastero donde vamos guardando todo lo que se nos ocurre). Si la aplicación Javascript es pequeña o si nosotros somos el único programador y no usamos librerías de terceros (algo casi imposible de evitar) la gestión de las variables globales es manejable. Pero es una excelente práctica evitar las variables globales siempre que sea posible.

Un ejemplo claro de esta buena práctica son las librerías jQuery: por más funciones que tengan y por más complejo que sea su código, no necesita usar ninguna variable global… excepto la propia variable jQuery (o su sinónimo $). Si hemos comprendido el ejemplo anterior, ahora sabemos que jQuery es equivalente a usar window.jQuery. Estas dos inicializaciones de jQuery son equivalentes:

Con una sola variable global jQuery (que nos sirve de punto de entrada para su funcionalidades), tenemos toda una excelente librería a nuestra disposición.

¿Cómo consigo aislar mis variables de las del resto de los programadores?

La respuesta, ya la adelanté en mi entrada acerca del manejo de las funciones en Javascript. En esta entrada, ya mencione a las “immediately-invoked function expression” (o «IIFE»). Como he mencionado antes, las variables declaradas dentro de una función son locales a dicha función y están absolutamente aisladas del resto del código. Así pues, para conseguir aislar nuestras variables… sólo tenemos que escribir nuestro código dentro de un IIFE:

He creado dos IIFE, cada una de las cuales tiene su variable privada name y que no tiene conflicto con la variable global que declaro al comienzo.

IMPORTANTE. Es un detalle muy simple, pero prefiero insistir: para que la IIFE se ejecute inmediatamente, hacen falta añadir los () finales. En el siguiente código, definimos la función… pero no se ejecuta:

¿Y si queremos acceder a la variable global name? Podemos acceder a ella a través de su identificativo window.name. Pero hay otro método que personalmente considero más elegante:

¿Qué es lo que hemos hecho? No olvidemos que una IIFE es una función. Y, como tal, admite parámetros. Pues, bien, hemos aprovechado esta característica para pasarle como parámetro la variable global name que necesito (al parámetro que la recibe lo he llamado pName).

Esta misma técnica, me la he encontrado muchas veces en código que la usa para cargar internamente las librerías Javascript que necesita. Por ejemplo, ya he mencionado que la librería jQuery está definida sobre la variable window.jQuery. Pues, bien, en lugar de usar esa variable global (algo, por otro lado, completamente legítimo) hay programadores que se la pasan a su propio código como parámetro parar usarla localmente:

Creación de closures

¿Qué es una closure? Vayamos poco a poco. En la entrada sobre funciones en Javascript que mencioné antes, escribí un ejemplo similar a éste:

El objetivo entonces era reflexionar sobre la posibilidad que tenemos de devolver objetos function como resultados de otras funciones. En este caso:

  • La función multiplier devuelve un objeto function.
  • Llamo dos veces a multiplier y guardo los dos resultados (o sea, los dos objetos) en dos variables double y triple.
  • Como ambas variables son objetos function, puedo invocarlas (pasándole a cada una el valor «2»).

Como he dicho, en esa entrada me concentré en la posibilidad de devolver objetos function (que pueden, por tanto, ser invocados posteriormente). Pero, ahora, me gustaría concentrarme en otro detalle: ¿por qué double(2) devuelve «4» y triple(2) devuelve «6»? Vamos a repasar el código detenídamente:

  1. Invoco a multiplier(2). El parámetro mult vale por tanto «2».
  2. Devuelvo un objeto function que recibe un parámetro num que multiplicará por mult y devolverá el resultado. Este objeto function, queda guardado en la variable double.
  3. Invoco a multiplier(3). Esta vez, el parámetro mult vale «3».
  4. Esta segunda invocación, devuelve otro objeto function también espera un parámetro num que también multiplicará por mult para devolver el resultado. Este segundo objeto function queda guardado en la variable triple.
  5. Cuando invoco double(2), se ejecuta el objeto function guardado en double: el parámetro num vale «2» y mult vale… ¿cuánto vale mult? Porque la última vez que ejecutamos la función multiplier, mult valía «3» ¿por qué vale «2» cuando ejecutamos double?

Lo que ocurre es que cada vez que se ejecuta la función multiplier se crea en la pila de variables una instancia de la variable mult:

  • En circunstancias normales, cuando termina la función, esta instancia de la variable se elimina de la pila.
  • Pero en este caso el objeto function que devolvemos sigue usando esta instancia de la variable mult (por lo que no puede ser eliminada de la pila).

Así, cada ejecución de la función multiplier crea en la pila su propia instancia de mult, cada una con su propio valor. Y cuando double y triple son ejecutadas cada una referencian a su propia instancia de la variable mult (que existirán mientras sigan existiendo las variables double y triple).

Este tipo de estructuras (un objeto function que es creado por una función y por tanto tiene acceso a la pila de variables de la función cuando ésta se ejecutó y el objeto function fue creado) recibe el nombre de closures.

Esta explicación que he dado, ha sido lo más directa posible. Pero me faltan muchos detalles. Si alguien está interesado en conocer este mecanismo en profundidad, le animo a leer este detallado artículo de Richard Cornford.

Cuando manejemos funciones anónimas y eventos, cuidado con estimar correctamente los valores de las variables

Bueno, como parece que tenemos controlado el concepto que acabo de explicar, vamos a realizar otro ejemplo. En este caso, quiero programar unas alarmas: una serie de mensajes que quiero que se me muestren en mi consola de depuración dentro de 1 segundo:

Si ejecuto este pequeño trozo de código y consulto mi consola de depuración, me aparecerán en 1 segundo dos mensajes «new alarm: undefined» (¡¡WTF!!).

Para depurarlo, me permito incluir una pequeña traza:

El mensaje que muestra la consola es «new alarm 2: undefined» ¿Por qué aparece un «2»? Porque ése es valor que tiene nuestra variable i cuando se ejecutan nuestras dos funciones anónimas. Desgraciadamente no debemos olvidar que las funciones anónimas se ejecutarán 1 segundo después de definirse dentro del bucle for… mucho después de que el bucle haya terminado (y para entonces nuestra variable i valdrá «2»).

De algún modo, para que nuestro código funcione como queremos, debemos conseguir «congelar» el valor de i para que cada función anónima muestre su correspondiente mensaje:

Vamos a analizar este código, paso a paso:

  • Creo una IIFE a la que le paso a su parámetro index el valor i: (function(index) { ... })(i)
  • Al pasar el valor i como parámetro, el valor que tiene en este momento concreto queda vinculado al parámetro index de esta IIFE particular que estoy creando.
  • Ahora, dentro de la IIFE, construyo y devuelvo un objeto function que será el que pase como parámetro a setTimeout (y que será el que mostrará el mensaje asociado a la alarma): return function() { console.debug('new alarm: ' + messages[index]); } (insisto que dentro de este objeto function el valor de index vale lo que vale i cuando es creada la IIFE).

En fin, ya sé que es un poco rebuscado. Pero si comenzáis a realizar código Javascript un poco sofisticado tarde o temprano os encontraréis con estructuras como ésta. Y es posible que también os encontréis con algún error relacionado con eventos y funciones anónimas que no hacen lo que teníais previsto y que no tenéis narices de depurar. Espero que entonces se os encienda la lucecita y os sirva este pequeño y retorcido trozo de código.

¿Conclusiones?

La estructura de variables de Javascript es relativamente simple. Y, como hemos visto, admite cierta relajación. El problema es que cuando el tamaño de nuestras aplicaciones Javascript comienza a crecer, esta «relajación» puede jugar en nuestra contra. En estos casos, lo mejor es activar el “modo estricto” y comenzar a analizar nuestro código con una herramienta como JSLint.

El encapsular todo nuestro código dentro de una IIFE es una excelente práctica. Cuesta poco hacerlo y permitirá que nuestro código coexista con otros códigos Javascript.

Por último, los closures son un recurso muy útil en Javascript. Pero hay que tener en cuenta el ciclo de vida de las variables que usemos dentro de estos closures para obtener el resultado adecuado.

Otro concepto al que me gustaría dedicarle una entrada es al contexto de ejecución de una función. Este concepto trata sobre la ejecución de funciones que son propiedades de objetos Javascript (lo que podíamos equiparar a «métodos» de otros lenguajes de programación… pero no es lo mismo). Me gustaría explicar cómo al ejecutarse dichas funciones pueden acceder a las otras propiedades del objeto donde se definieron… siempre que se cumplan ciertas reglas. Y eso obliga a explicar qué es el objeto this que tantas veces aparece en el código Javascript. Explicar estos conceptos, también están muy relacionados con los mecanismos que nos proporciona Javascript para crear clases (o algo parecido). Pero eso será ya otra historia…

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

*
*
Sitio Web