Ir al contenido principal

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:

  1. Fase de compilación: JavaScript realiza un primer análisis del código, identificando todas las declaraciones de variables y funciones.
  2. 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 y const 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 por let y const

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.