Herramientas de usuario

Herramientas del sitio


informatica:programacion:cursos:programacion_avanzada_javascript:declaracion_variables_let_const

Declaración de variables con let y const

Módulo perteneciente al curso Programación avanzada con JavaScript y ECMAScript.

ECMAScript

La sintaxis y capacidades del lenguaje JavaScript se encuentran reguladas por el organismo ECMA (European Computer Manufacturer Association - Asociación Europea de Fabricantes de Computadoras). Se trata de una organización sin ánimo de lucro que se encarga, entre otras cosas, de regular el funcionamiento de muchos estándares de la industria mundial, no solo de Europa. Entre estos estándares se encuentran los de algunos lenguajes de programación, como por ejemplo C# (que es el estándar ECMA-334), las extensiones del lenguaje C++/CLI, el lenguaje de descripción de servicios Web (WSDL) o, por supuesto, nuestro queridísimo ECMAScript que es el estándar ECMA-262.

JavaScript es una de las implementaciones del estándar ECMA-262, en concreto la que se usa en los navegadores. Pero existen otras implementaciones con sus propias extensiones, como por ejemplo ActionScript, el lenguaje que se usaba para programar Flash que se lanzó también en 1997.

La primera versión de JavaScript, ECMAScript 1, se lanzó en Junio de 1997, y desde entonces han existido las versiones 2, 3 y 5 (la 4 se abandonó), que es la que se conoce generalmente como simplemente JavaScript. Durante varios años ECMA estuvo trabajando en la siguiente versión del lenguaje, conocida como ECMAScript 6, y en junio de 2015 por fin vio la luz la versión definitiva, con el nombre oficial ECMAScript 2015.

Se trata de una evolución del lenguaje JavaScript para dotarlo de características avanzadas que se echaban mucho en falta y que sí estaban disponibles en otros lenguajes populares, como por ejemplo:

  • Mejoras de sintaxis: parámetros por defecto, let, plantillas…
  • Módulos para organización de código
  • Sintaxis de “verdaderas” clases para programación orientada a objetos
  • Promesas, para programación asíncrona
  • Mejoras en programación funcional: expresiones de flecha, iteradores, generadores…

En Junio de 2016 ECMA lanzó una nueva versión del lenguaje, ECMAScript 2016 también conocida por algunos como ECMAScript 7, pero no es el ES7 que te piensas. En esta revisión sin embargo las novedades son mínimas, pero alguna hay.

Posteriormente, cada año se han ido introduciendo novedades en el lenguaje. En este completo segundo bloque se estudian a fondo todas y cada una de las novedades relevantes que han ido introduciendo las sucesivas versiones de ECMAScript, hasta la más reciente, y cómo aplicarlas tanto en navegadores que las soportan de manera nativa, como en navegadores más antiguos mediante el uso de transpiladores.

Entre otras cosas aprenderás:

  • Nuevas funciones y tipos de datos
  • Nuevas formas de declarar variables y su efecto en el ámbito, this, etc…
  • Nuevos tipos de colecciones y agrupamientos de información
  • Promesas
  • Operador flecha, pérdida de this, uso de lambdas
  • Mejoras y cambios en definición y notación de objetos
  • Uso de literales de cadena y plantillas
  • Símbolos y sus aplicaciones avanzadas
  • Weakmaps
  • Fetch
  • Async/await
  • Desestructuración de datos y el operador “spread”
  • Nuevos tipos de bucles
  • Iteradores y generadores
  • Programación orientada a objetos con ECMAScript
  • Creación y uso de proxies.
  • Modularización de código
  • Transpilación a ES5

¡Adelante!

Hoisting

El hoisting es una característica que muchos desarrolladores que han estado usando el lenguaje desconocen. Consiste en que, con independencia de donde declaremos una variable, la declaración se mueve al principio del ámbito. Además debemos tener presente que JavaScript tiene tan solo dos ámbitos: ámbito global o ámbito local (de función). No existe, como en otros lenguajes, el ámbito de bloque (aunque esto último cambia, como vamos a ver en este módulo).

Ámbito: Región de código en la cual una variable es accesible a través de su identificador. Decimos que una variable “sale de ámbito” cuando deja de ser accesible. JavaScript ha tenido siempre dos ámbitos: local y global. EcmaScript 2015 añade el ámbito de bloque.

¿Qué crees que imprime el siguiente código?

'use strict';
var n = 'eiximenis';
function foo() {
    n = 'campusMVP';
    var n = null;
    for (var i=0; i<10; i++) {
        n = 'iter ' + i;
    }
}
foo();
console.log(n);

Este código no da error. Se puede observar el uso del modo estricto, para que el motor de JavaScript nos avise de que accedemos a cualquier variable no declarada previamente. Se puede ver que la primera línea de la función foo accede a una variable n. Lo normal es pensar que esta variable n a la que se accede es la variable global cuyo valor es eiximenis. Posteriormente, se declara una variable local con el mismo nombre n. Sabemos que las variables locales “ocultan” a las globales si tienen el mismo nombre. Por lo tanto las asignaciones dentro del for afectan a la variable local, no a la global.

Visto así, lo lógico es pensar que al final de este código la variable local valdrá “iter 9” y la variable global tendrá el valor de “campusMVP”. Pero eso no es así. Al finalizar este código la variable global sigue teniendo el valor de “eiximenis. ¿Cómo se entiende eso? Pues por el hoisting.

Recuerda que el hoisting significa que todas las declaraciones de variables se mueven al principio del ámbito correspondiente (y recuerda que aparte del global, el único ámbito existente es el local). Así, el código anterior es como si realmente se hubiese escrito:

'use strict';
var n = 'eiximenis';
function foo() {
    var n;              // La declaración se mueve al principio de la función
    n = 'campusMVP';
    n = null;
 
    for (var i=0; i<10; i++) {
        n = 'iter ' + i;
    }
}
foo();
console.log(n);

Leyendo este código queda claro que la asignación del valor ”campusMVP” es a la variable local, no a la global, y por lo tanto, se puede ver claramente que el valor de la variable global nunca se modifica dentro de la función foo. Como se puede comprobar en el ejemplo anterior, el uso del modo estricto no te previene de los efectos del hoisting.

Otra forma de definir el hoisting consiste en decir que dentro de un ámbito concreto (local, global, bloque) un identificador en concreto (es decir un nombre de variable) siempre hace referencia a la misma variable (por eso en el ejemplo, dentro del ámbito local el identificador “n” siempre hace referencia a la misma variable: la variable local “n”, aunque ésta esté definida posteriormente al código que accede a ella). En lenguajes que no tienen hoisting un mismo nombre de variable puede referenciar a variables distintas en función de cuando se use este nombre. Todas las declaraciones en JavaScript tienen hoisting, incluidas las variables declaradas con las nuevas palabras clave “let” y “const”, a pesar de que en algunos sitios puedas leer lo contrario. En la lección siguiente se amplía esta información.

Declaración de variables con let

En ES2015 se introduce una nueva palabra clave, let, que se puede usar para declarar variables. A todos los efectos let es como var pero con dos diferencias importantes:

  • Las variables declaradas con let no se ven afectadas por el hoisting. Realmente, eso no es del todo cierto (más detalles a continuación) aunque lo verás escrito en muchos sitios porque los efectos finales son muy parecidos.
  • Las variables declaradas con let tienen ámbito de bloque.

Una nota (un pelín técnica) sobre el tema del hoisting y let. Realmente a todas las declaraciones en JavaScript (var, let, const, function, class) se les aplica el hoisting. La sutileza está en que las variables definidas por let (y const, como veremos) al declararse están en un estado, conocido como dead zone, en el cual su valor no vale undefined, sino que es un error de referencia. Es decir, acceder a una variable que está en dead zone da un error de referencia. Una variable solo puede salir de este estado cuando se le asigna un valor. Es por esto, que técnicamente, decimos que no hay hoisting porque el comportamiento es casi equivalente. Y en el “casi” está la clave: una variable declarada con let ocultará a una variable de ámbito superior del mismo nombre, incluso antes de ser declarada (dentro de su ámbito correspondiente), aunque acceder a ella produzca un error si este acceso ocurre antes de su declaración. Eso sí, es importante saber que el hoisting se aplica según el ámbito de visibilidad: es decir, una variable declarada con let tiene hoisting al inicio de su bloque, mientras que una variable declarada con var tiene hoisting siempre al inicio de la función (dado que var no tiene visibilidad restringida por bloque).

La nota anterior, aunque técnica, es importante así que vamos a dar algunos detalles al respecto. P. ej. el siguiente código produce un error:

var g = 10;
var f = function() { 
   console.log(g); 
   let g=20;
}
f();    // ReferenceError al acceder a g antes de su declaración

Dentro de la función f, el console.log debería imprimir el valor 10 si no hubiera realmente hoisting (observa que si g estuviese definida con var y no con let el console.log imprimiría “undefined” y no daría error, precisamente por el hoisting). Pero, realmente hay hoisting de la variable g, por lo que este console.log no imprime el valor de la variable global, sino que lo hace de la variable local (definida más abajo). Pero al estar definida con let, el valor obtenido por el hoisting no es indefinido sino que es no válido, ya que la variable está en su dead zone. Así, al acceder a ella obtenemos el ReferenceError.

Por simplificación leerás en muchos sitios que las variables declaradas con let no sufren del hoisting, pero es un poco “aceptamos pulpo”: como simplificación sirve, pero no es exacto. Ahora ya sabes qué sucede con exactitud.

Ámbito de bloque

El ámbito de bloque implica que una variable declarada dentro de un bloque de código (tal y como un for o un if) es visible y existe solo dentro de ese bloque de código. Eso evita posibles errores y además puede disminuir el consumo de memoria: cuando se sale de un bloque de código, todos los objetos creados en él, pueden ser destruidos (ya que pasarán a ser inaccesibles).

Como hemos visto, si se accede a una variable declarada con let antes de su declaración, se obtiene un error de referencia. Este comportamiento es como, si para las variables declaradas con let, se aplicase el modo estricto (en el sentido de que se nos impide acceder a una variable no declarada). Por ello algunos entornos de ECMAScript (como V8 usada, entre otros, por Chrome y NodeJS) no permiten usar let sin el modo estricto. Así el siguiente código da error de referencia:

'use strict';
function foo() {
    var sum = 0;
    for (let i=0; i< 10; i++) {
        sum += i;
    }   
    console.log('iters ->' + i + ' result ' + sum);      Error de referencia: i ya no existe en este punto
}
foo();

El siguiente código da error también:

console.log(v);
let v=10;

En este caso, al intentar acceder a la variable v antes de su declaración provoca un error, ya que la variable es como si no existiese (está en su dead zone tal y como hemos analizado antes).

Declaraciones let duplicadas

A diferencia de var, una variable declarada con let no puede ser declarada dos veces. El siguiente código funciona correctamente e imprime el valor 20 en la consola:

function foo() {
   var a=10;
   var a=20;
   console.log(a);
}
foo();

Pero el mismo código usando let en lugar de var genera un error:

De hecho es importante resaltar que el error se da incluso sin llamar a la función foo , ya que no es un error de ejecución sino que se produce en la fase de análisis semántico.

Análisis semántico: Fase, posterior al análisis sintáctico, en la que un compilador valida que el código es semánticamente válido. En esta fase, se comprueban condiciones en las cuales construcciones sintácticamente válidas son semánticamente incorrectas (p. Ej. Acceder a una variable antes de su declaración o validaciones de tipos).

DEMO: Uso de let

var idx = 10;
console.log(idx);
 
for (var idx = 0; idx <= 20; idx++) {
}
 
console.log(idx);

Al ejecutar el código anterior, veremos por consola el valor '10' y luego el 21. Aunque estemos declarando la variable idx en el bucle for, al usar var realmente lo que se hace es redeclarar la idx que teníamos al principio, que es de ámbito global.

Pero si hacemos lo siguiente:

var idx = 10;
console.log(idx);
 
for (let idx = 0; idx <= 20; idx++) {
}
 
console.log(idx);

Al ejecutar el código, veremos por consola el valor '10' y luego el '10'. El segundo console.log sigue mostrando el valor de la variable idx declarada con var y no muestra el valor de idx declarada en el bucle for porque esta última tiene ámbito de bloque y solo tiene sentido dentro de dicho bucle.

Declaración de constantes con const

Otra de las novedades de ECMAScript 2015 es la posibilidad de definir constantes usando const. Cuando usamos const para declarar una constante debemos asignarle el valor en la misma declaración:

const PI = 3.14159;

Por curioso que pueda parecer, asignar un valor nuevo a una constante no da error en todos los entornos aunque el valor no es asignado (es decir, la constante mantiene su valor original). Así en V8 (Chrome y NodeJS) el siguiente código imprime 10 pero no da error:

const a = 10;
a = 20;
console.log(a);     // Imprime 10 y no 20.

Que la asignación de un valor a una constante no genere error, y al mismo tiempo no modifique el valor asignado, es coherente con el funcionamiento de las propiedades de solo lectura de los objetos en JavaScript: a una propiedad de solo lectura, se le puede asignar un valor pero dicha asignación es ignorada. De todos modos, en Firefox este código da un error (TypeError). Así pues, no asumas que deba suceder algo concreto al asignar un valor a una constante, simplemente debes presuponer que a esa constante nunca se le asignará un valor nuevo.

En el momento de escribir el curso, Safari permite cambiar el valor de una constante (es decir, el código anterior imprimiría 20 y no 10 (como debería) al usar Safari). Este comportamiento no es correcto ya que rompe la semántica de const y es considerado un bug.

Constantes e inmutabilidad

El uso de const permite declarar constantes, pero JavaScript sigue sin tener un soporte directo para valores inmutables: a una constante no se le puede volver a asignar un valor, pero si una constante contiene un objeto, se pueden modificar las propiedades de dicho objeto, sin ningún problema:

const c = {value: 42};
c.value = 20;
console.log(c.value);    // Imprime 20

Recuerda pues, que lo que const te garantiza es que a la constante no se le volverá asignar un nuevo valor, pero no te asegura que el valor de la constante se modifique (si es un objeto sus propiedades pueden cambiar y si es un array se le pueden agregar o eliminar elementos).

Constantes en ECMAScript 5

A pesar de que en ES5 no existe soporte directo para constantes, es posible simularlas en cierta manera. Eso sí, la restricción es que todas las constantes “simuladas” viven en el contexto global, ya que la forma de simularlas es, precisamente, crear una propiedad de solo lectura dentro de este contexto global:

Object.defineProperty(typeof global === "object" ? global : window, "Answer", {
    value:        42,
    enumerable:   true,
    writable:     false
});

El código anterior define una propiedad (Answer) de solo lectura en el ámbito global. El ámbito global se llama global bajo NodeJS y se llama window en navegadores, de ahí la comprobación de la primera línea de código.

La propiedad es declarada como “no configurable” (es decir, básicamente, no puede eliminarse) pero no uses el parámetro configurable:false en la llamada a Object.defineProperty, porque algunos navegadores dan error si se usa el parámetro configurable con el valor false al definir propiedades sobre el ámbito global (a pesar de ser, paradójicamente, el valor por defecto de dicho parámetro).

Así, si no usas el parámetro configurable, la propiedad creada es ya no configurable, como puede apreciarse en la siguiente captura:

Con este mecanismo puedes simular constantes globales, pero no constantes de ámbito local. Otra diferencia importante es que las constantes declaradas con const tienen visibilidad de ámbito, al igual que las variables declaradas con let y siguen las mismas reglas referentes al hoisting.

Cuándo usar var, let o const

En esta sección vamos a discutir brevemente, cuando usar var, let o const.

var o let

Tanto var como let permiten declarar variables. Las diferencias principales son, que las variables declaradas con let por defecto existen en la dead zone y por lo tanto acceder a ellas antes de su declaración produce un error, y que tienen ámbito de bloque.

Si se usa let para declarar una variable global, su comportamiento es casi idéntico al de var, lo mismo que si se usa para declarar una variable local a una función (en ambos casos la única diferencia es que las variables declaradas con var tienen hoisting). A no ser que, por alguna oscura razón, quieras beneficiarte del hoisting, es mejor usar siempre let antes que var.

Hay otra diferencia entre var y let y tiene que ver con la captura de variables por parte de una closure. Observa el fichero closure_var.html de las descargas de esta lección y ábrelo en un navegador. Luego abre el fichero closure_let.html, haz clic en los distintos “divs” y compara los resultados.

closure: Función que captura (es decir, tiene acceso) los valores (variables locales) de la función que la contiene. En JavaScript las funciones anónimas actúan automáticamente como closures.

En el ejemplo que usa var existe un solo objeto i y todas las closures capturan el mismo objeto: es por ello que todas terminan imprimiendo el mismo valor. Por otro lado en el ejemplo que usa let cada closure captura su “propio” objeto i y por ello cada una imprime un valor distinto. Lo que se suele desear es el comportamiento ofrecido por let, no el ofrecido por var, y se solía solucionar pasando el valor de i por parámetro. Ahora con let eso ya no es necesario.

Fichero closure_var.html:

<!DOCTYPE html>
<html>
	<head>
		<title>Demo closures var</title>
		<style type="text/css">
			div {
				border:1px solid #001fa6
			}
		</style>
	</head>
	<body>
		<h1>Closures usando var</h1>
		<hr>
		<div id="id1">Div id1  - Click me</div>
		<div id="id2">Div id2  - Click me</div>
		<div id="id3">Div id3  - Click me</div>
		<div id="id4">Div id4  - Click me</div>
		<div id="id5">Div id5  - Click me</div>
		<div id="id6">Div id6  - Click me</div>
		<script>
			for (var i = 1; i<=6; i++) {
				document.getElementById("id" + i).addEventListener("click", 
					function() {
						console.log('click -> ' + i);
					}
				, false);
			}		
		</script>
	</body>
</html>

Fichero closure_let.html:

<!DOCTYPE html>
<html>
	<head>
		<title>Demo closures let</title>
		<style type="text/css">
			div {
				border:1px solid #001fa6
			}
		</style>
	</head>
	<body>
		<h1>Closures usando let</h1>
		<hr>
		<div id="id1">Div id1  - Click me</div>
		<div id="id2">Div id2  - Click me</div>
		<div id="id3">Div id3  - Click me</div>
		<div id="id4">Div id4  - Click me</div>
		<div id="id5">Div id5  - Click me</div>
		<div id="id6">Div id6  - Click me</div>
		<script>
			'use strict';
			for (let i = 1; i<=6; i++) {
				document.getElementById("id" + i).addEventListener("click", 
					function() {
						console.log('click -> ' + i);
					}
				, false);
			}
		</script>
	</body>
</html>

Let o const

Entre let y const el dilema es relativamente simple: usa const siempre que sea posible. Recuerda que const simplemente evita que se pueda asignar un nuevo valor a una variable, pero no evita que el valor contenido pueda mutar (si es un objeto o un array). Usar const deja más clara la intención de no reasignar el valor contenido por la variable.

informatica/programacion/cursos/programacion_avanzada_javascript/declaracion_variables_let_const.txt · Última modificación: por tempwin