Hoisting
Introducción
Cuando escribimos código JavaScript, normalmente esperamos que se ejecute línea por línea, de arriba hacia abajo. Sin embargo, JavaScript tiene un comportamiento particular llamado hoisting (elevación, en español) que puede sorprendernos si no lo conocemos bien. El hoisting hace que ciertas declaraciones sean "elevadas" o movidas al principio de su ámbito durante la fase de compilación, antes de que el código se ejecute realmente. Este comportamiento afecta principalmente a las declaraciones de variables y funciones, y es crucial entenderlo para evitar errores y comportamientos inesperados en nuestro código. En este artículo, exploraremos en detalle qué es el hoisting, cómo afecta a diferentes elementos de JavaScript y cómo trabajar adecuadamente con él.
Definición del concepto de hoisting
El término "hoisting" proviene del inglés "to hoist", que significa elevar o izar. En JavaScript, el hoisting es un mecanismo por el cual las declaraciones (no las inicializaciones) de variables y funciones son conceptualmente movidas al inicio de su ámbito durante la fase de compilación, antes de que se ejecute el código.
Para entender bien el hoisting, es importante conocer cómo JavaScript procesa el código:
- Fase de compilación: JavaScript realiza un primer análisis del código, identificando todas las declaraciones de variables y funciones.
- Fase de ejecución: El código se ejecuta línea por línea, con las declaraciones ya procesadas en la fase anterior.
El hoisting ocurre durante la fase de compilación. Aunque el código no se mueve físicamente, se comporta como si las declaraciones estuvieran al inicio de su ámbito.
Hoisting de declaraciones de funciones
Las declaraciones de funciones se elevan completamente. Esto significa que podemos llamar a una función antes de que aparezca su declaración en el código:
// Podemos llamar a la función antes de declararla
saludar("Ana"); // "Hola, Ana"
// Declaración de función (se eleva completamente)
function saludar(nombre) {
console.log(`Hola, ${nombre}`);
}
En este ejemplo, la función saludar
está disponible en todo su ámbito, incluso antes de su declaración, porque la declaración completa (con su nombre, parámetros y cuerpo) se eleva al inicio.
Es importante distinguir entre declaraciones de funciones y expresiones de funciones:
// Esto funciona (declaración de función)
funcionDeclarada(); // "Soy una función declarada"
function funcionDeclarada() {
console.log("Soy una función declarada");
}
// Esto NO funciona (expresión de función)
funcionExpresion(); // Error: funcionExpresion is not a function
var funcionExpresion = function() {
console.log("Soy una expresión de función");
};
Las expresiones de función se comportan como variables, por lo que solo se eleva la declaración, pero no la asignación de la función.
Hoisting de variables con var
Las declaraciones de variables con var
también se elevan, pero a diferencia de las funciones, solo se eleva la declaración, no la inicialización:
console.log(nombre); // undefined (no Error)
var nombre = "Carlos";
Lo que ocurre realmente es equivalente a:
var nombre; // Declaración elevada
console.log(nombre); // undefined
nombre = "Carlos"; // Inicialización en su posición original
Este comportamiento puede llevar a confusiones, ya que la variable existe pero tiene el valor undefined
hasta que se ejecuta la línea de inicialización.
Hoisting en bucles
El hoisting también afecta a las variables en bucles:
for (var i = 0; i < 3; i++) {
console.log(`Dentro del bucle: ${i}`);
}
console.log(`Fuera del bucle: ${i}`); // 3
La variable i
declarada con var
es elevada al ámbito de la función contenedora (o al ámbito global si no hay función), por lo que sigue existiendo fuera del bucle.
Comportamiento de let y const
Con la introducción de ES6, llegaron las declaraciones let
y const
, que tienen un comportamiento diferente respecto al hoisting:
console.log(nombre); // Error: Cannot access 'nombre' before initialization
let nombre = "Carlos";
Aunque técnicamente las declaraciones con let
y const
también son elevadas, se comportan de manera diferente debido a lo que se conoce como Zona Muerta Temporal (Temporal Dead Zone o TDZ). La variable existe en el ámbito desde el principio, pero no se puede acceder a ella hasta que se ejecuta la línea de declaración.
Esto proporciona un comportamiento más predecible y ayuda a evitar errores comunes:
// Con var
function ejemploVar() {
console.log(contador); // undefined
var contador = 1;
}
// Con let
function ejemploLet() {
console.log(contador); // Error: Cannot access 'contador' before initialization
let contador = 1;
}
Diferencias entre ES5 y ES6+
La introducción de ES6 trajo cambios significativos en el comportamiento del hoisting:
ES5 (JavaScript tradicional)
- Variables declaradas con
var
se elevan al inicio de su ámbito (función o global) - El valor inicial es
undefined
- Las funciones declaradas se elevan completamente
ES6+ (JavaScript moderno)
- Variables declaradas con
let
yconst
también se elevan, pero permanecen en la TDZ hasta su declaración - Intentar acceder a ellas antes de la declaración produce un error
- El ámbito de bloque (
{}
) es respetado porlet
yconst
Implicaciones prácticas en el código
El hoisting puede tener importantes implicaciones en nuestro código:
1. Orden de declaración de variables
// Confuso y propenso a errores
function calcularPrecio() {
precio = precioBase + impuesto; // Usando variables antes de declararlas
console.log(`Precio final: ${precio}€`);
var precioBase = 100;
var impuesto = 21;
var precio;
}
// Más claro y seguro
function calcularPrecio() {
var precioBase = 100;
var impuesto = 21;
var precio = precioBase + impuesto;
console.log(`Precio final: ${precio}€`);
}
2. Redeclaración de variables
var mensaje = "Hola";
// ... más código ...
var mensaje = "Adiós"; // Redeclaración permitida con var
let contador = 1;
// ... más código ...
let contador = 2; // Error: Identifier 'contador' has already been declared
3. Sombra de variables en bloques
var edad = 30;
if (true) {
var edad = 40; // Misma variable, sobrescribe el valor anterior
}
console.log(edad); // 40
let altura = 180;
if (true) {
let altura = 175; // Nueva variable con el mismo nombre, limitada al bloque
}
console.log(altura); // 180
Cómo evitar problemas relacionados
Para evitar problemas relacionados con el hoisting, podemos seguir estas prácticas:
1. Declarar variables al inicio de su ámbito
function procesarDatos() {
// Todas las declaraciones al inicio
let datos = [];
let resultado;
let i;
// Resto del código
datos = obtenerDatos();
// ...
}
2. Preferir let
y const
sobre var
Las declaraciones con let
y const
son más predecibles y reducen errores comunes:
// Usar const para valores que no cambiarán
const PI = 3.14159;
const URL_API = "https://api.ejemplo.com";
// Usar let para variables que necesitan reasignación
let contador = 0;
let usuario = null;
// Evitar var en código moderno
// var elemento = document.getElementById("miElemento"); // Evitar
let elemento = document.getElementById("miElemento"); // Preferible
3. Usar modo estricto
El modo estricto ('use strict'
) ayuda a detectar errores comunes y prohíbe algunas características problemáticas:
'use strict';
function ejemplo() {
x = 10; // Error: x is not defined (en modo estricto)
}
4. Usar funciones expresadas cuando sea apropiado
Las expresiones de función pueden ser más predecibles en ciertos contextos:
// Declaración (hoisting completo)
function sumar(a, b) {
return a + b;
}
// Expresión (asignada a una constante)
const restar = function(a, b) {
return a - b;
};
// Función flecha (más concisa)
const multiplicar = (a, b) => a * b;
Buenas prácticas de declaración
Para escribir código más claro y evitar sorpresas relacionadas con el hoisting:
1. Usa una declaración por variable
// Evitar
var a = 1, b = 2, c = 3;
// Preferible
let a = 1;
let b = 2;
let c = 3;
2. Utiliza nombres descriptivos
// Evitar
let x = 5;
// Preferible
let cantidadProductos = 5;
3. Mantén el ámbito lo más reducido posible
// Evitar variables globales o con ámbito muy amplio
let resultado;
function procesarDatos() {
resultado = calcular(); // Modifica variable externa
}
// Preferible: ámbito reducido
function procesarDatos() {
let resultado = calcular();
return resultado;
}
4. Utiliza IIFE para encapsular código
Las expresiones de función inmediatamente invocadas (IIFE) pueden ayudar a encapsular variables y evitar la contaminación del ámbito global:
(function() {
// Variables locales a esta IIFE
var mensaje = "Hola";
let contador = 0;
// Funciones y lógica
function incrementar() {
contador++;
console.log(mensaje, contador);
}
incrementar();
})();
// mensaje y contador no están disponibles aquí
Ejemplos prácticos de hoisting
Ejemplo 1: Problema clásico con var
function ejemploHoisting() {
console.log(a); // undefined (no error)
console.log(b); // undefined (no error)
console.log(c); // Error: c is not defined
var a = 1;
var b; // Solo declaración
// c no está declarado en absoluto
console.log(a); // 1
console.log(b); // undefined
}
ejemploHoisting();
Ejemplo 2: Funciones y expresiones de función
// Esto funciona
console.log(suma(5, 3)); // 8
function suma(a, b) {
return a + b;
}
// Esto NO funciona
console.log(resta(5, 3)); // Error: resta is not a function
var resta = function(a, b) {
return a - b;
};
Ejemplo 3: Let y la Zona Muerta Temporal
function ejemploTDZ() {
// Inicio de la TDZ para la variable nombre
console.log(nombre); // Error: Cannot access 'nombre' before initialization
let nombre = "Ana"; // Fin de la TDZ
console.log(nombre); // "Ana"
}
Resumen
El hoisting es un comportamiento fundamental de JavaScript que "eleva" las declaraciones de variables y funciones al inicio de su ámbito durante la fase de compilación. Las declaraciones de funciones se elevan completamente, mientras que las declaraciones de variables con var
se elevan, pero se inicializan con undefined
. Las variables declaradas con let
y const
también se elevan, pero permanecen inaccesibles (en la Zona Muerta Temporal) hasta su declaración.
Entender el hoisting es esencial para escribir código JavaScript predecible y evitar errores comunes. Las mejores prácticas incluyen declarar variables al inicio de su ámbito, preferir let
y const
sobre var
, y ser consciente de las diferencias entre declaraciones de funciones y expresiones de funciones.
En el próximo artículo, profundizaremos en otro concepto fundamental de JavaScript: las clausuras (closures), que están estrechamente relacionadas con el ámbito y permiten patrones de programación muy poderosos.