El alcance de las variables usando var

Figura
Figura. Título del estándar ECMAScript® 2015 (ES6).

EcmaScript versión 6 (abreviado como ES6) es ya una realidad. Se refiere al Standard ECMA-262 6th Edition / June 2015 que define un estándar para lenguajes como JavaScript para los navegadores o Node.js para los servidores. Trae muchas cosas nuevas y que conviene aprender cuanto antes. Por mi parte empezaré a experimentar con cosas sueltas que me interesan y que iré exponiendo en este sitio. En este tema veremos como afecta el nuevo estándar a la declaración de variables.

Lo primero que aprendemos con JavaScript es que tenemos que declarar las variables con var. Pero también sabíamos que una asignación a una variable sin usar var la ubicaba en el espacio global. El espacio global de variables referencia el mismo conjunto de propiedades del objeto window. Así si por error omitíamos var podíamos sobrescribir una propiedad ya existente en window en caso de que tuviese el mismo nombre.

En este primer ejemplo tenemos una función funcionScope() que se ejecuta con la carga de la página. Asigna un valor a una variable miVarGlobal sin usar var. Luego declara otra variable miVarLocal asignándole otra cadena. Esta variable tiene un alcance local a la función donde se encuentra, por lo que fuera de la misma no está definida. Mientras que la otra tiene un alcance global, es accesible desde cualquier sitio.

Cuando pulsemos el botón en el ejemplo se ejecutará la función verVariables() obteniendo los valores de ambas variables. En esta función controlamos con try-catch el error que se producirá al intentar acceder a una variable.

//Esta función se ejecuta con la carga de la página
wxL.funcionScope = function(){
    miVarGlobal = "Soy un String ubicado en Global";
    var miVarLocal = "Soy un String ubicado en funcionScope()";
};
//Y esta cuando usamos el botón "Ver variables"
wxL.verVariables = function() {
    document.getElementById("mi-var-global").textContent = '"' + miVarGlobal + '"';
    try {
        document.getElementById("mi-var-local").textContent = miVarLocal;
    } catch(e){
        document.getElementById("mi-var-local").textContent = e.message;
    }
};    
    

Ejemplo: Alcance con declaración var

miVarGlobal =
miVarLocal =

La variable global es accesible desde cualquier sitio. La local sólo dentro de la función donde se declara con var. Si intentamos acceder fuera de su alcance nos dará el error de que la variable no está definida como vemos en el ejemplo. En el Developer Tools del navegador podemos ver donde se ubicaron:

Figura
Figura. El alcance Global y Local de las variables declaradas con var.

En el espacio global hay un montón de variables y funciones, todas ellas inicialmente propias del navegador. Las que ubiquemos por no usar var también van a parar ahí. Podríamos sobrescribir algunas de las existentes y el resultado sería imprevisible. Además a la hora de depurar una página nos resultaría más trabajoso diferenciar nuestras propias variables. Por lo tanto es estrictamente necesario usar siempre var para declarar variables dentro del cuerpo de una función.

Si declaramos una variable por fuera de una función, es decir, en el cuerpo principal del Script, con o sin var también van a parar al espacio global. La solución más simple para evitarlo es la siguiente:

<script>
    //Estas dos, con o sin var, van al espacio global
    miVarGlobal = 1;
    var otraVarGlobal = 1;  
    //Encerrando el código del módulo en una función autoejecutable
    //evitamos el alcance global de las variables declaradas con var 
    //en el cuerpo del módulo  
    (function(){
        //Código del módulo
        var miVarLocal = 2;
        function abc(){
            var otraVarLocal = 3;
        }
    })();
</script>
    

Encerrando todo el módulo en una función autoejecutable, denominadas también IIFE (immediately-invoked function expression), conseguimos aislar sus variables. Si aún necesitamos variables globales es mejor considerar el uso de un espacio de nombres.

El modo estricto en JavaScript (Strict Mode)

El modo estricto de JavaScript se definió en la versión anterior ES5 y sigue vigente en la actual ES6. Usando la cadena "use strict" en el inicio de una función o un script supone que la ejecución en ese alcance de algunas características se acogerán a la especificación. En el caso de las variables declaradas sin usar var nos advertirá de un error. El modo estricto nos obliga a usar siempre var.

wxL.verGlobalStrict = function(){
    "use strict";
    var valor;
    try {
        miVarGlobalStrict = "Soy OTRO String ubicado en Global";
        valor = miVarGlobalStrict;
    } catch(e){
        valor = e.message;
    }
    document.getElementById("mi-var-global-strict").innerHTML =  valor;
};
    

En este ejemplo se ejecutará la función verGlobalStrict() cuando pulsemos el botón. En el momento en que el navegador intente realizar la asignación a la variable miVarGlobalStrict sin anteponerle var se generará un error.

Ejemplo: Modo estricto impide variables globales sin usar var

miVarGlobalStrict =

Los mensajes de error obtenidos en los navegadores son algo diferentes:

  • En Chrome 48: miVarGlobalStrict is not defined.
  • En Firefox 43: assignment to undeclared variable miVarGlobalStrict.
  • En IE11: Variable sin definir en modo strict.

A pesar de las diferencias todos quieren decir lo mismo, no podemos acceder a la variable miVarGlobalStrict pues está sin definir. En IE nos da mensajes en español, indicando claramente que sucede en modo estricto. El mensaje más descriptivo es el de Firefox, diciéndonos que estamos haciendo una asignación a una variable no declarada. Ambos términos variable no declarada o variable sin definir se refieren a lo mismo: la variable no existe en ese punto del código y por tanto no se le puede hacer una asignación.

Es una buena cosa que algo nos impida declarar variables sin usar var. Por ahora sólo lo podemos conseguir haciendo uso de la directiva "use strict". Es soportado por los navegadores tal como se observa en Caniuse Strict Mode. Pero antes de usarla profusamente tendría que estudiar un poco más a fondo las repercusiones que pudiera tener en el JavaScript de este sito. Entretanto podría haber otra solución y es, a partir de ahora, dejar de usar var y en su lugar utilizar let y const.

Declarando variables con let y const de ES6 para evitar el Hoisting

Vimos antes que es una desventaja de JavaScript el poder declarar variables sin usar var pues van a parar al espacio global. Pero también hay otra y es el hoisting de las variables. Podemos traducirlo como alzamiento de las variables y ahora entenderemos la razón. En JavaScript uno puede declarar una variable en cualquier línea del cuerpo de una función o módulo. Pero cuando se ejecuta ese alcance, JavaScript busca todas las declaraciones de variables y las reubica al inicio del cuerpo, como si las hubiesemos declarado inicialmente. Por ejemplo, podemos tener el siguiente código:

function fun(x) {
    if (x>0) x = x+1;
    var miVar = x-5;
}
    

Uno podría pensar que la variable miVar no existe hasta que lleguemos a la línea donde se declara. Pero para JavaScript y antes de iniciar la ejecución ese código se transforma en el siguiente:

function fun(x) {
    var miVar;
    if (x>0) x = x+1;
    miVar = x-5;
}
    

Por lo tanto todas las variables de la función ya están implícitamente declaradas antes de iniciarse la ejecución de la primera línea de esa función. Declaradas pero no iniciadas, recordando que cuando declaramos una variable sin asignarle ningún valor JavaScript le asigna el valor undefined. Veámos esto en un ejemplo:

Ejemplo: Hoisting no aplica a let y const

function funcionVar() {
    //"use strict""use strict";
    console.log(typeof miVar); //Tipo es undefined
    var miVar = "Soy un String";
    console.log(typeof miVar); //Tipo es string
}
Tipo de variable

Notas

Antes de ejecutar este ejemplo debe activarlo con el botón anterior. Este script se ejecuta al cambiar la declaración var, let o const, usando las funciones funcionVar(), funcionLet() y funcionConst() respectivamente, funciones ubicadas en un script que se crea dinámicamente en esta página. Lo hacemos así para evitar la falta de soporte de las nuevas declaraciones de variables en algunos navegadores y que no ocasionen un error de código que paralice otras funcionalidades del JavaScript de esta página.

Firefox 44 soporta las nuevas declaraciones. Si su navegador es Firefox 43 o inferior, antes de activar el ejemplo debe activar la casilla para incluir el tipo "application/javascript; version=1.7" en el script que creamos dinámicamente. Si su navegador no soporta ese MimeType no le aparecerá esa opción. Sobre el soporte actual de estas nuevas declaraciones let y const vea el último apartado de este tema.

El resultado del typeof realmente no se saca por la consola sino que sobrescribimos el elemento que representa el comentario resaltado del código anterior. Debería funcionar según lo esperado en Chrome 48+, Firefox 43+ e IE11+. Chrome 48 necesita el modo estricto para poder usar let y const.

Este ejemplo muestra el uso de la tres declaraciones de variables con var, let y const que ahora ya se permiten en ES6. Consultamos con typeof el tipo de la variable declarada con var. Esto lo podemos hacer antes y después de la declaración de la variable debido al hoisting, dado que miVar fue alzada al inicio del cuerpo de la función. Por eso el tipo antes es undefined dado que la variable existe sin valor asignado. Y después es string pues ya tiene un valor de ese tipo asignado.

Lo que nos interesa es ver que no se aplica hoisting a las variables declaradas con let y const. Antes de la declaración al consultar el tipo aparece un error indicando que la variable no existe. Estos son los mensajes de error que vemos en cada navegador:

  • En Chrome 48: miVar is not defined.
  • En Firefox 43: can't access lexical declaration 'miVar' before initialization.
  • En IE11: Usar antes de declaración.

Veáse que Chrome nos dice que la variable no está definida, que no es lo mismo que la variable tenga el valor undefined, en cuyo caso si estaría definida pero sin valor asignado. En definitiva, los tres mensajes se refieren a lo mismo, no se puede acceder a una variable antes de ser declarada. Y esto creo que es bueno, porque uno espera que las variables no existan antes de ser declaradas.

Olvidar el efecto del hoisting sobre var suele conducir a errores díficiles de detectar. En el siguiente código uno podría esperar que si x es falso la función devuelva el valor de la variable miVar que está en el alcance exterior a la función.

var miVar = 0;
function fun(x) {
    if (x){
        var miVar = 1;
        return miVar;
    }
    return miVar;
}
console.log(fun(false)); // ¿Devuelve 0?
    

Pero ese código se transforma en el siguiente por el hoisting, donde es evidente que sea cual sea el valor de x lo que estamos retornando es la variable miVar del alcance de la función. Es más, con var no hay forma de acceder a la variable externa si ya hay otra con el mismo nombre en la función, sea cual sea su ubicación.

var miVar = 0;
function fun(x) {
    //El hoisting declara la variable al inicio
    //de la función con valor undefined...
    var miVar;
    if (x){
        //...y esto es una simple asignación
        miVar = 1;
        return miVar;
    }
    return miVar;
}
console.log(fun(false)); // Devuelve undefined
    

Por eso se recomienda declarar y asignar valores a todas las variables de una función al inicio de la misma. Pero es verdad que es más cómodo ir haciéndolo a medida que las vamos necesitando. Usando var el hoisting las ubica al inicio sin asignar, por lo que no se genera un error al usar una variable de esta clase en cualquier línea del código de una función dado que ya existe y tiene valor (como mínimo undefined).

Parece que al final llegamos a la conclusión de que el hoisting no es una buena cosa. Y esa es otra razón para usar let y const en lugar de var.

El alcance de las variables usando let y const

Las variables declaradas con var tienen un alcance a la función donde se declaran. Las nuevas declaraciones let y const pueden tener además un alcance de bloque. Un bloque es el código encerrado entre llaves {...}, como el que sigue al condicional en este ejemplo:

if (x) {
    var a = 1;
    let b = 2;
}
console.log(a); // 1 si x es true, undefined en otro caso
console.log(b); // Error en cualquier caso: b no existe
    

La variable b deja de existir por fuera del bloque donde se declaró. JavaScript extiende el concepto de alcance de bloque a los bucles de iteración for, formando parte también del alcance la declaración de variables en la expresión previa al bloque. Vea este ejemplo:

Ejemplo: Alcance de bloque

function funcionBlockVar() {
    "use strict";
    var i = 0;
    var j = i+1;
    for (var i=0; i<10; i++){
        var j = i+1;
    }
    console.log(i); // i = 10
    console.log(j); // j = 10
}
        
Tipo de variable

Notas

Antes de ejecutar este ejemplo debe activarlo con el botón anterior. Este script se ejecuta al cambiar la declaración var o let, usando las funciones funcionBlockVar() y funcionBlockLet() respectivamente, funciones ubicadas en un script que se crea dinámicamente en esta página. Lo hacemos así para evitar la falta de soporte de las nuevas declaraciones de variables en algunos navegadores y que no ocasionen un error de código que paralice otras funcionalidades del JavaScript de esta página.

Firefox 44 soporta las nuevas declaraciones. Si su navegador es Firefox 43 o inferior, antes de activar el ejemplo debe activar la casilla para incluir el tipo "application/javascript; version=1.7" en el script que creamos dinámicamente. Si su navegador no soporta ese MimeType no le aparecerá esa opción. Sobre el soporte actual de estas nuevas declaraciones let y const vea el último apartado de este tema.

El resultado de los valores i, j realmente no se sacan por la consola sino que sobrescribimos los elementos que representan los comentarios resaltados del código anterior. Debería funcionar según lo esperado en Chrome 48+, Firefox 43+ e IE11+: con var obtendríamos i=10, j=10 mientras que con let sería i=0, j=1. Chrome 48 necesita el modo estricto para poder usar let y const.

En el ejemplo tenemos las variables i y j a nivel de bloque y en el bucle for. Con var y por el hoisting todas las declaraciones se unifican en una sóla al inicio del cuerpo de la función. Las siguientes declaraciones var sobre la misma variable se comportarán como simples asignaciones. Por eso cuando finalizamos la ejecución de esa función ambas variables valen 10, pues el bucle finaliza al tomar i ese valor y en la iteración anterior j tomó también ese valor.

Con let el alcance de las variables en el for las aisla del exterior de ese bloque. Por eso la i,j en el ejemplo valen 1 y 0 respectivamente, valores de las variables en el alcance de la función. Las del bucle for, incluidas las declaradas en la expresión entre paréntesis, dejan de existir fuera de ese bloque.

La ausencia de alcance de bloque era algo que se echaba de menos en JavaScript. Por lo tanto esta es otra razón para usar let y const en lugar de var.

Comportamiento específico de let en bucles for

Los bucles for nos dan muchas veces sorpresas con la variable declarada con var que usamos para iterar por el bucle. Veámos un simple código como el siguiente.

for (var i=0; i<10; i++){
    //hacer algo
}
    

Por el hoisting ese código realmente se ejecuta como el siguiente, donde en cada iteración lo único que estamos haciendo es una nueva reasignación de la variable i.

var i;
for (i=0; i<10; i++){
    //hacer algo
}
    

Esto puede producir un closure indeseado si dentro del bucle declaramos una función. Supongamos que tenemos que crear diez botones en un documento. Queremos adjudicarle eventos click de tal forma que al pulsar sobre uno de ellos nos devuelva su número de orden, tarea ideal para usar un bucle for.

for (var i=0; i<10; i++){
    var boton = document.createElement("button");
    boton.type = "button";
    boton.textContent = "botón";
    boton.addEventListener("click", function(){
        console.log(i); // Siempre será 10
    }, false);
    document.body.appendChild(boton);
}
    

Sin embargo todos los botones nos darán el valor i = 10, valor con el que la variable finalizó el bucle. Tal como explicamos en el enlace sobre closures, esto es debido a que la variable i queda blindada en el closure de cada función de evento. Estas funciones harán referencia a la misma i, variable que al finalizar el código tiene el valor 10. En aquel tema vimos como resolverlo: devolviendo una función interna para quitar la atadura del closure de la función externa.

for (var i=0; i<10; i++){
    var boton = document.createElement("button");
    boton.type = "button";
    boton.textContent = "botón";
    boton.addEventListener("click", function(n){
        return function(){
            console.log(n); // n=0,1,2,...,9
        };
    }(i), false);
    document.body.appendChild(boton);
}
    

Ahora ES6 le da un comportamiento específico a let en la variable que itera en un bucle for, característica que permite resolver el problema anterior sin más artilugios:

"use strict";
for (let i=0; i<10; i++){
    var boton = document.createElement("button");
    boton.type = "button";
    boton.textContent = "botón";
    boton.addEventListener("click", function(){
        console.log(i); // i=0,1,2,...,9
    }, false);
    document.body.appendChild(boton);
}
    

Ese código es igual al que nos ocasionaba el problema, sólo que cambiando var por let en la variable iteradora. En cada iteración ahora se realiza explícitamente una nueva declaración de i, con lo que cada función queda atada a una variable diferente, cada una con su valor. Esto es algo bueno que nos trae ES6 y nos empuja a abandonar el usar de var en beneficio de let cuando trabajemos con bucles.

Variables globales con let y const no se ubican en window

Figura
Figura. Declaraciones con let y const no ubica las variables en el objeto window.

Las variables declaradas con var en el cuerpo del script van a parar al espacio global de las variables. Este espacio se identifica con el objeto window en el JavaScript de los navegadores. En Node.js no existe window pues dispone de un específico objeto global para eso. Con let y const las variables del espacio global no se identifican en window, lo que es una buena noticia para no llenarlo con un montón de variables.

En el siguiente ejemplo declaramos la variable AAA con var y, tal como se observa en la Figura, vemos que la ubica en el alcance (Scope) Global, que también se identifica con Window en el navegador. Para las variables declaradas con let y const se crea en el Developer Tools el nuevo alcance Script. Son también globales a efectos de todo el JavaScript de una página, pero no están en window.

"use strict";
var AAA = "Soy VAR global";
let AAB = "Soy LET global";
const AAC = "Soy CONST global";
function miFuncion() {
    console.log(AAA); // "Soy VAR global"
    console.log(AAB); // "Soy LET global"
    console.log(AAC); // "Soy CONST global"
    console.log(window.AAA); // "Soy VAR global"
    console.log(window.AAB); // undefined
    console.log(window.AAC); // undefined
}
miFuncion();
   

Hay que tener en cuenta que cuando recuperamos una propiedad del objeto window podemos prescindir de esta referencia. Es decir, es lo mismo escribir window.alert(1) que sólo alert(1). Así que la variable global AAA declarada con var viene a ser finalmente una propiedad del objeto window en los navegadores. Y podemos acceder a ella con window.AAA o sólo con AAA. Y esto, en mi opinión, es una particularidad de JavaScript que no hace más que entorpecer la claridad del código. Otra razón más para usar las nuevas declaraciones en lugar de var.

Con let y const no se puede redeclarar la misma variable

Con var podemos usar múltiples veces la declaración de la misma variable. En este código redeclaramos una variable un par de veces sin ningún problema:

var a = 1;
console.log(a); // 1
var a = "abc";
console.log(a); // "abc"
    

Pero con let y const esto es un error de sintaxis, con lo cual el código no se ejecuta en absoluto.

let a = 1;
console.log(a);
let a = "abc"; // Syntax error: Identifier 'a' has already been declared
console.log(a);
    

Esto es bueno pues en códigos largos no sabremos si estamos volviendo a declarar una variable declarada ya anteriormente, cuando la finalidad era crear una nueva variable. Cuantas veces nos habremos entretenido un rato en un código que falla porque estábamos machacando sin querer una declaración de variable anterior.

Constantes en JavaScript ES6

Una gran cantidad de variables no necesitan ser modificadas en la ejecución de un código. Lo óptimo es entonces declararlas como constantes. No sólo desde el punto de vista conceptual eso sería lo adecuado, sino que además evitamos posibles errores de reasignación. En ese caso JavaScript nos advertirá un error de sintaxis y nos detendrá el código. Vea este ejemplo usando const.

"use strict";
const miConstante = 1;
try {
    miConstante = 2;
} catch(e){
    console.log(e.message); // Error de asignación a constante
}
    

Los navegadores ofrecen mensajes de error como estos:

  • Chrome 48: Assignment to constant variable.
  • Firefox 43: SyntaxError: invalid assignment to const miConstante.
  • IE11: Asignación a const.

Los objetos también pueden ser constantes, pero con la particularidad de que lo que no podemos modificar es a lo que apunta la referencia, pero si podemos modificar su contenido. En el siguiente ejemplo Vemos que podemos modificar valores y agregar nuevas propiedades, pero no podemos reasignar la constante de nuevo.

"use strict";
const miObjeto = {a: 1, b: 2};
miObjeto.a = 3; // Modificando un valor
miObjeto.c = 4; // Agregando otra propiedad
console.log(miObjeto); // Object {a: 3, b: 2, c: 4}
try {
    miObjeto = {c: 3, d: 4}; // Reasignando a otro objeto
} catch(e){
    console.log(e.message); // Error de asignación a constante
}
    

Hay quien opina que debemos usar const para declarar todas las variables inicialmente, a no ser que sean objeto de modificación posterior, en cuyo caso usaríamos let. Pero esto parece algo engorroso, pues por un lado nos obliga a tener que estar pensando si posteriormente la variable fuera objeto de modificación. Podríamos usar const y seguir escribiendo código, pero si después cambiamos de opinión hemos de volver atrás para hacer el cambio por let.

La otra perspectiva sería usar siempre let y reservar const para las verdaderas constantes. Uno piensa en ellas como las constantes matemáticas, como π que siempre vale 3.141592..., más cómodo usarla con su nombre que no con su valor. Por ejemplo, en el script general que uso para este sitio tengo una constante denominada NOMBRES_MES que uso para unas funciones de fechas, conteniendo los nombres de los meses en español.

Es corriente usar mayúsculas para nombrar las constantes, pues en cualquier parte del código se diferencian adecuadamente. Antes haría var MI_VALOR = 123.45; y a partir de ahora const MI_VALOR = 123.45;. Estas constantes suelen usarse a nivel de módulo, ubicándose al inicio del mismo para una fácil localización y que estén disponibles en todo su alcance.

Usar let y const y no volver a usar var

Tras experimentar todos los ejemplos se concluye que es aconsejable dejar de usar var por let y const. Pero es evidente que no podemos sustituir sin más var por las nuevas declaraciones en los Script que ya tenemos escritos. Una estrategia podría ser empezar a hacer el cambio en los nuevos códigos que escribamos. Pues sería una tarea inabordable revisar a fondo un montón de código sólo para hacer ese cambio.

Sin embargo actualmente hemos de tener en cuenta algunas cosas para empezar a usar let y const:

  • Se necesita la directiva "use strict" para Chrome 48. Si se lo ponemos a todos los códigos nuevos tenemos primero que aprender que efecto causa. Los navegadores que no soporten esa directiva la ignorarán no produciendo error.
  • IE11+ soporta las nuevas declaraciones con o sin modo estricto.
  • En Firefox 43 no se soporta por defecto, pero si lo hace la versión Firefox 44 que acaba de salir precisamente en estos días (26 enero 2016). La versión 44 permite las nuevas declaraciones con o sin modo estricto, tal como IE11+. Con la versión 43 hay que usar <script type="application/javascript; version=1.7"> para que funcione. La versión puede ser 1.7 o superior. Pero no podemos hacer esto para todos los navegadores, puesto que la parte que indica la versión no es estándar y hará que otros navegadores no reconozcan el MimeType del script y, por tanto, no lo ejecuten.