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ística | Declaración | Expresión | Flecha |
| Hoisting | Sí | No | No |
Propio this | Sí | Sí | No (hereda) |
| Uso como método de objeto | Recomendado | Recomendado | No recomendado |
| Sintaxis | Verbosa | Media | Concisa |
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
- Existen tres tipos de funciones: declaraciones (con hoisting), expresiones (sin hoisting), y funciones flecha (sin propio
this).
- El alcance determina la visibilidad de las variables: global, de función, y de bloque (
let/const).
- Un closure es una función que recuerda las variables de su entorno de creación, permitiendo datos privados y estado persistente.
- Las IIFE se ejecutan inmediatamente y crean un alcance aislado, útiles para evitar contaminar el ámbito global.
this en funciones normales depende del contexto de llamada; las funciones flecha heredan el this del ámbito exterior.
- map, filter y reduce son funciones de orden superior esenciales para trabajar con arrays de forma funcional y expresiva.
- Los operadores rest y spread permiten trabajar con número variable de argumentos y copiar/combinar arrays y objetos fácilmente.