Programación Web · Capítulo 9

JavaScript Avanzado: Funciones, Closures y Alcance

Domina los conceptos que separan a los programadores principiantes de los profesionales: el alcance, los closures y las funciones de orden superior.


1. Tipos de Funciones en JavaScript

JavaScript es un lenguaje donde las funciones son "ciudadanos de primera clase" (first-class citizens). Esto significa que las funciones pueden asignarse a variables, pasarse como argumentos a otras funciones y retornarse desde funciones. Existen tres formas principales de definir una función.

Declaraciones de Función (Function Declarations)

Una declaración de función tiene la palabra clave function seguida de un nombre. Son hoisted (elevadas), lo que significa que puedes llamarlas antes de que aparezcan en el código.

// Declaración de función — puede llamarse ANTES de su definición saludar("Ana"); // Funciona gracias al hoisting function saludar(nombre) { return "Hola, " + nombre + "!"; } console.log(saludar("Carlos")); // "Hola, Carlos!"

Expresiones de Función (Function Expressions)

Una función se asigna a una variable. No son hoisted, por lo que deben definirse antes de usarse.

// Expresión de función — NO puede llamarse antes de su definición const multiplicar = function(a, b) { return a * b; }; console.log(multiplicar(4, 5)); // 20 // Intentar llamarla antes causaría un ReferenceError // multiplicar(4,5); // ❌ Error si está antes de la definición

Funciones Flecha (Arrow Functions)

Introducidas en ES6, las funciones flecha ofrecen una sintaxis más concisa y tienen un comportamiento especial con la palabra clave this (lo veremos más adelante).

// Función flecha básica const sumar = (a, b) => a + b; console.log(sumar(3, 7)); // 10 // Con un solo parámetro, los paréntesis son opcionales const cuadrado = x => x * x; console.log(cuadrado(5)); // 25 // Con cuerpo de bloque necesitas return explícito const dividir = (a, b) => { if (b === 0) return "Error: división por cero"; return a / b; }; console.log(dividir(10, 2)); // 5
CaracterísticaDeclaraciónExpresiónFlecha
HoistingNoNo
Propio thisNo (hereda)
Uso como método de objetoRecomendadoRecomendadoNo recomendado
SintaxisVerbosaMediaConcisa

2. Alcance (Scope): Dónde Viven las Variables

El alcance determina desde qué partes del código se puede acceder a una variable. Hay tres tipos principales.

Alcance Global

Variables declaradas fuera de cualquier función o bloque. Son accesibles desde cualquier parte del código. Deben usarse con moderación para evitar contaminación del espacio de nombres.

const PI = 3.14159; // Alcance global function calcularCircunferencia(radio) { return 2 * PI * radio; // Puede acceder a PI } console.log(calcularCircunferencia(5)); // 31.4159

Alcance de Función

Variables declaradas con var dentro de una función. Solo existen dentro de esa función.

function miFuncion() { var mensaje = "Soy local"; // Solo existe dentro de miFuncion console.log(mensaje); // "Soy local" } miFuncion(); // console.log(mensaje); // ❌ ReferenceError: mensaje is not defined

Alcance de Bloque (Block Scope)

Con let y const (ES6+), las variables viven dentro del bloque {} donde se declaran. Esto incluye bucles, condicionales, y cualquier bloque.

function ejemploBloques() { let x = 10; // Alcance de función (bloque exterior) if (true) { let y = 20; // Solo existe en este bloque if const z = 30; // Solo existe en este bloque if console.log(x); // 10 ✅ puede ver x console.log(y); // 20 ✅ } console.log(x); // 10 ✅ // console.log(y); // ❌ ReferenceError } // Diferencia importante con var en bucles for (var i = 0; i < 3; i++) { /* i escapa al bucle */ } console.log(i); // 3 — var NO tiene alcance de bloque for (let j = 0; j < 3; j++) { /* j está confinado */ } // console.log(j); // ❌ ReferenceError — let SÍ tiene alcance de bloque

3. Closures: La Característica Más Poderosa de JavaScript

Definición de Closure: Un closure es una función que "recuerda" el entorno en el que fue creada, incluso después de que ese entorno haya terminado de ejecutarse. La función interior tiene acceso a las variables de la función exterior.

Los closures ocurren cuando una función es definida dentro de otra función y hace referencia a variables de la función exterior. Esta es la base de muchos patrones avanzados en JavaScript.

Ejemplo Básico: El Contador

function crearContador() { let conteo = 0; // Esta variable "vive" en el closure return function() { conteo++; return conteo; }; } const contador = crearContador(); console.log(contador()); // 1 console.log(contador()); // 2 console.log(contador()); // 3 // Creamos otro contador independiente const otroContador = crearContador(); console.log(otroContador()); // 1 — independiente del primero

¿Por qué funciona? Cuando llamamos crearContador(), la función se ejecuta y retorna la función interior. Normalmente, la variable conteo desaparecería. Pero la función retornada "captura" la referencia a conteo en su closure. Cada vez que llamamos al contador, conteo persiste.

Variables Privadas con Closures

function crearCuentaBancaria(saldoInicial) { let saldo = saldoInicial; // Variable privada — no accesible desde afuera return { depositar(monto) { if (monto <= 0) return "Monto inválido"; saldo += monto; return `Depositado: $${monto}. Saldo actual: $${saldo}`; }, retirar(monto) { if (monto > saldo) return "Fondos insuficientes"; saldo -= monto; return `Retirado: $${monto}. Saldo actual: $${saldo}`; }, consultarSaldo() { return `Saldo: $${saldo}`; } }; } const miCuenta = crearCuentaBancaria(1000); console.log(miCuenta.consultarSaldo()); // "Saldo: $1000" console.log(miCuenta.depositar(500)); // "Depositado: $500. Saldo actual: $1500" console.log(miCuenta.retirar(200)); // "Retirado: $200. Saldo actual: $1300" // console.log(miCuenta.saldo); // undefined — ¡saldo es privado!

Patrón Módulo (Module Pattern)

El patrón módulo usa closures para crear código encapsulado con estado privado y una API pública.

const moduloCarrito = (function() { // Estado privado const items = []; // API pública return { agregar(producto, precio) { items.push({ producto, precio }); console.log(`${producto} agregado al carrito`); }, total() { return items.reduce((sum, item) => sum + item.precio, 0); }, listar() { return items.map(i => `${i.producto}: $${i.precio}`).join("\n"); } }; })(); // IIFE — se ejecuta inmediatamente moduloCarrito.agregar("Libro", 25); moduloCarrito.agregar("Cuaderno", 10); console.log(moduloCarrito.total()); // 35 console.log(moduloCarrito.listar()); // Libro: $25 // Cuaderno: $10

4. IIFE — Expresiones de Función Invocadas Inmediatamente

Una IIFE es una función que se define y se ejecuta al mismo tiempo. Sirve para crear un alcance privado y evitar contaminar el ámbito global.

// Sintaxis básica de IIFE (function() { const variablePrivada = "Solo vivo aquí"; console.log(variablePrivada); // "Solo vivo aquí" })(); // variablePrivada no existe aquí ✅ // IIFE con parámetros (function(nombre, año) { console.log(`Hola ${nombre}, año ${año}`); })("Mundo", 2024); // IIFE con función flecha const resultado = (() => { const x = 10; const y = 20; return x + y; })(); console.log(resultado); // 30

5. La Palabra Clave this

El valor de this depende de cómo se llama a la función, no de dónde se define. Este es uno de los conceptos más confusos de JavaScript.

// 1. En el contexto global (no estricto), this = window/global console.log(this); // Window (en navegador) // 2. En un método de objeto, this = el objeto const persona = { nombre: "María", saludar() { return `Hola, soy ${this.nombre}`; } }; console.log(persona.saludar()); // "Hola, soy María" // 3. Las funciones flecha NO tienen su propio this const equipo = { nombre: "Los Campeones", miembros: ["Ana", "Luis", "Pedro"], mostrar() { // Sin flecha: this perdería el contexto dentro de forEach this.miembros.forEach(miembro => { console.log(`${miembro} pertenece a ${this.nombre}`); }); } }; equipo.mostrar(); // Ana pertenece a Los Campeones // Luis pertenece a Los Campeones // Pedro pertenece a Los Campeones

6. Funciones de Orden Superior y Métodos de Array

Una función de orden superior es una función que recibe otra función como argumento, retorna una función, o ambas. Los métodos map, filter y reduce son los más utilizados en JavaScript moderno.

map() — Transformar cada elemento

const precios = [100, 250, 80, 320, 150]; // Aplicar 20% de descuento a todos los precios const preiosConDescuento = precios.map(precio => precio * 0.8); console.log(preiosConDescuento); // [80, 200, 64, 256, 120] // Transformar objetos const productos = [ { nombre: "Camisa", precio: 150 }, { nombre: "Pantalón", precio: 300 }, { nombre: "Zapatos", precio: 450 } ]; const nombresConIVA = productos.map(p => ({ nombre: p.nombre, precioConIVA: p.precio * 1.16 })); console.log(nombresConIVA); // [{ nombre: "Camisa", precioConIVA: 174 }, ...]

filter() — Seleccionar elementos

const calificaciones = [45, 78, 92, 61, 38, 85, 71, 95, 52]; // Solo los aprobados (>= 60) const aprobados = calificaciones.filter(cal => cal >= 60); console.log(aprobados); // [78, 92, 61, 85, 71, 95] // Solo los reprobados const reprobados = calificaciones.filter(cal => cal < 60); console.log(reprobados); // [45, 38, 52] // Filtrar objetos const estudiantes = [ { nombre: "Ana", activo: true }, { nombre: "Luis", activo: false }, { nombre: "Pedro", activo: true } ]; const activos = estudiantes.filter(e => e.activo); console.log(activos.map(e => e.nombre)); // ["Ana", "Pedro"]

reduce() — Acumular un valor

const ventas = [1200, 3400, 800, 2100, 4500]; // Suma total const total = ventas.reduce((acumulador, venta) => acumulador + venta, 0); console.log(total); // 12000 // Encontrar el máximo const maximo = ventas.reduce((max, venta) => venta > max ? venta : max, 0); console.log(maximo); // 4500 // Agrupar por categoría const items = [ { categoria: "Ropa", monto: 200 }, { categoria: "Electrónica", monto: 800 }, { categoria: "Ropa", monto: 150 }, { categoria: "Electrónica", monto: 1200 } ]; const porCategoria = items.reduce((acc, item) => { acc[item.categoria] = (acc[item.categoria] || 0) + item.monto; return acc; }, {}); console.log(porCategoria); // { Ropa: 350, Electrónica: 2000 }

Encadenamiento de map, filter y reduce

const empleados = [ { nombre: "Ana", departamento: "IT", salario: 45000 }, { nombre: "Luis", departamento: "Ventas", salario: 32000 }, { nombre: "Pedro", departamento: "IT", salario: 52000 }, { nombre: "María", departamento: "IT", salario: 48000 }, { nombre: "Carlos", departamento: "Ventas", salario: 38000 } ]; // Salario promedio del departamento IT const promedioIT = empleados .filter(e => e.departamento === "IT") .map(e => e.salario) .reduce((sum, sal, _, arr) => sum + sal / arr.length, 0); console.log(promedioIT); // 48333.33...

7. Parámetros Avanzados: Default, Rest y Spread

Parámetros por Defecto

function crearPerfil(nombre, edad = 18, rol = "estudiante") { return `${nombre}, ${edad} años, ${rol}`; } console.log(crearPerfil("Ana")); // "Ana, 18 años, estudiante" console.log(crearPerfil("Luis", 25)); // "Luis, 25 años, estudiante" console.log(crearPerfil("Pedro", 30, "admin")); // "Pedro, 30 años, admin"

Operador Rest (...)

// Recibir cualquier número de argumentos function sumarTodos(...numeros) { return numeros.reduce((sum, n) => sum + n, 0); } console.log(sumarTodos(1, 2, 3)); // 6 console.log(sumarTodos(10, 20, 30, 40)); // 100 // Rest después de parámetros normales function registrarEvento(evento, fecha, ...participantes) { console.log(`Evento: ${evento}, Fecha: ${fecha}`); console.log(`Participantes: ${participantes.join(", ")}`); } registrarEvento("Taller", "2024-03-15", "Ana", "Luis", "Pedro", "María");

Operador Spread (...)

// Expandir arrays const arr1 = [1, 2, 3]; const arr2 = [4, 5, 6]; const combinado = [...arr1, ...arr2]; console.log(combinado); // [1, 2, 3, 4, 5, 6] // Copiar sin mutar const original = [1, 2, 3]; const copia = [...original]; copia.push(4); console.log(original); // [1, 2, 3] — sin cambios console.log(copia); // [1, 2, 3, 4] // Expandir objetos (Object Spread) const config = { tema: "oscuro", idioma: "es" }; const configCompleta = { ...config, fontsize: 14, debug: false }; console.log(configCompleta); // { tema: "oscuro", idioma: "es", fontsize: 14, debug: false } // Pasar array como argumentos individuales const nums = [5, 3, 8, 1, 9]; console.log(Math.max(...nums)); // 9

Resumen del Capítulo