Hasta ahora has trabajado con variables, tipos, comparaciones, template literals y bucles. Todas esas piezas tienen un problema en común: si necesitas repetir la misma lógica en dos sitios del código, tienes que copiarla. Cuando esa lógica cambia, tienes que encontrar todas las copias y actualizarlas.
Las funciones resuelven eso. Encapsulan un fragmento de lógica con un nombre, y a partir de ahí puedes reutilizarla cuantas veces quieras: la escribes una vez y, si cambia, la corriges en un único sitio. En la industria a esta idea se la conoce como principio DRY (Don’t Repeat Yourself, «no te repitas»).
Qué es una función#
Una función es un bloque de código con nombre al que puedes llamar con (). Cuando
la llamas, ejecuta su cuerpo y, opcionalmente, devuelve un valor:
function saludar() {
// imprime el mensaje en la consola
console.log('Hola, equipo');
}
// ejecuta el bloque → imprime "Hola, equipo"
saludar();Sin funciones, cada operación repetiría el mismo código. Con funciones, lo escribes una vez y lo reutilizas.
Las tres formas de definir una función#
En JavaScript hay tres formas habituales. Las tres son equivalentes en lo esencial; la diferencia está en cuándo se pueden usar y qué conviene según el contexto.
Declaración de función#
La forma más clásica. Tiene hoisting: el motor de JavaScript la eleva al inicio del scope antes de ejecutar ninguna línea. Eso significa que puedes llamarla antes de que aparezca en el código y funcionará sin errores.
// Esta llamada aparece ANTES de la definición de la función.
// Funciona porque las declaraciones de función se elevan al inicio.
const resultado = calcularWinrate(78, 120);
// muestra el resultado: 0.65 — no hay error aunque la función se define más abajo
console.log('Winrate: ' + resultado);
// La definición puede estar abajo: el motor la procesa antes de ejecutar nada.
// victorias y partidas son los parámetros
function calcularWinrate(victorias, partidas) {
// devuelve el cociente al código que llamó a la función
return victorias / partidas;
}Las expresiones de función y las arrow functions no tienen hoisting: si las llamas antes de la línea donde están asignadas, obtienes un error. La declaración es la única forma que se puede invocar antes de aparecer.
Es la forma más legible para funciones nombradas que van a reutilizarse en varios sitios.
Expresión de función#
// La función es un valor asignado a la variable calcularWinrate.
// mismos parámetros
const calcularWinrate = function(victorias, partidas) {
// devuelve el cociente, igual que la declaración
return victorias / partidas;
};
// Sin hoisting: solo se puede llamar después de esta asignación.
// Si intentas llamarla antes de esta línea, obtienes un ReferenceError.
// devuelve 0.65 — funciona porque aparece DESPUÉS de la definición
const resultado = calcularWinrate(78, 120);Aquí la función es un valor asignado a una variable const. No hay hoisting:
si la llamas antes de la línea donde se define, obtendrás un error. Se usa cuando
quieres que la función sea un dato que puedes pasar a otra función o asignar después.
Arrow function#
// Cuerpo de una sola expresión: las llaves y el return se omiten.
// victorias / partidas se devuelve automáticamente.
const calcularWinrate = (victorias, partidas) => victorias / partidas;
// mismo resultado que las dos formas anteriores: 0.65
const resultado = calcularWinrate(78, 120);La forma más compacta. Cuando el cuerpo es una sola expresión, puedes omitir las
llaves y el return: el resultado de esa expresión se devuelve automáticamente.
Es la sintaxis estándar en 2026 para funciones cortas y para los callbacks que
verás cuando lleguemos a los métodos de array.
¿Cuándo usar cada una? En la práctica: declaraciones para funciones principales y reutilizables, arrow functions para transformaciones cortas y callbacks.
Una diferencia que verás más adelante: las arrow functions no tienen su propio
this; lo heredan del entorno donde las escribes. Eso importa al trabajar con métodos de objeto y eventos, y lo veremos a fondo en los capítulos de clases y dethis. Por ahora quédate con esto: para funciones cortas y callbacks, la arrow es justo lo que quieres.
Parámetros y argumentos#
Los parámetros son los nombres que pone quien escribe la función. Los argumentos son los valores que pasa quien la llama:
// victorias y partidas son parámetros: nombres que elige quien escribe la función.
function calcularWinrate(victorias, partidas) {
// calcula el cociente y lo devuelve
return victorias / partidas;
}
// 78 y 120 son los argumentos: valores concretos que pasa quien llama.
// llama a la función con esos argumentos → resultado = 0.65
const resultado = calcularWinrate(78, 120);Los parámetros también pueden tener valores por defecto, que se usan cuando
el argumento no se pasa o se pasa undefined:
// partidas tiene valor por defecto: 1
function winrateSeguro(victorias, partidas = 1) {
// divide victorias entre partidas y devuelve el resultado
return victorias / partidas;
}
// partidas no se pasó → usa el defecto 1 → 78 / 1 = 78
winrateSeguro(78);
// partidas = 120 → sobreescribe el defecto → 78 / 120 = 0.65
winrateSeguro(78, 120);Los valores por defecto documentan qué es opcional y evitan divisiones por cero o comportamientos inesperados cuando los datos llegan incompletos.
return: devolver un valor#
return termina la función y devuelve el valor indicado al código que la llamó.
Una función puede tener varios return (uno por cada rama de un if, por ejemplo),
pero solo se ejecuta el primero que se alcance.
// recibe un número entre 0 y 1
function clasificarWinrate(winrate) {
// sale aquí si se cumple: no se evalúan los siguientes
if (winrate >= 0.6) return 'bueno';
// solo se llega aquí si winrate < 0.6
if (winrate >= 0.5) return 'aceptable';
// return final: se ejecuta si ninguna condición anterior se cumplió
return 'por mejorar';
}Si una función no tiene return, o llega a un return sin valor, devuelve
undefined. Esto es un error habitual: olvidar el return en una función que
se usa para calcular algo y luego preguntarse por qué el resultado es undefined.
function winrateSinReturn(victorias, partidas) {
// se calcula, pero no se devuelve
const winrate = victorias / partidas;
// Olvidamos el return: la función termina sin devolver nada
}
// undefined — el cálculo se perdió dentro
console.log(winrateSinReturn(78, 120));La norma práctica: si una función calcula algo, devuélvelo con return. Quien llama
decide si imprimirlo, guardarlo o usarlo en otra operación.
Scope: dónde viven las variables#
El scope (o alcance) determina desde qué partes del código es visible una variable. En JavaScript hay tres niveles:
Scope global: una variable declarada fuera de cualquier función o bloque es accesible desde cualquier sitio del fichero. Hay que usarlo con precaución: si dos partes del código modifican la misma variable global, se pisan entre sí.
// global: visible desde cualquier función
const equipoNombre = 'Overwatch Team';
function mostrarEquipo() {
// accede a la variable global → 'Overwatch Team'
console.log(equipoNombre);
}
function cambiarEquipo() {
// Si aquí se modificara equipoNombre, afectaría a todas las demás funciones.
// también la ve → 'Overwatch Team'
console.log(equipoNombre);
}Scope de función: una variable declarada con const o let dentro de una
función solo existe dentro de esa función. Cuando la función termina, la variable
desaparece.
function calcularWinrate(victorias, partidas) {
// solo existe dentro de esta función
const ratio = victorias / partidas;
// se devuelve el valor, pero la variable ratio desaparece al salir
return ratio;
}
// ReferenceError: ratio no existe fuera de la función
console.log(ratio);Scope de bloque: una variable declarada con const o let dentro de un bloque
{} (un if, un for, etc.) solo vive mientras ese bloque se ejecuta.
function evaluarPartida(victorias, partidas) {
// existe en toda la función
const winrate = victorias / partidas;
if (winrate >= 0.5) {
// solo existe dentro de este if
const mensaje = 'Por encima del 50%';
// funciona: estamos dentro del bloque
console.log(mensaje);
}
// ReferenceError: mensaje no existe fuera del if
console.log(mensaje);
}Esa última línea provoca a propósito un ReferenceError: JavaScript intenta usar la
variable mensaje en un punto donde ya no existe (vivía solo dentro del if) y detiene
el programa con un error de ese tipo en la consola. No es un fallo tuyo: lo forzamos aquí
para que veas dónde acaba el alcance de una variable de bloque.
El scope no es una restricción arbitraria: es lo que hace que cambiar una función no rompa nada en otra parte del código. Cada función trabaja con sus propias variables; el exterior no puede pisarlas.
Closures: una función que recuerda su entorno#
Un closure ocurre cuando una función se crea dentro de otra y conserva acceso a las variables del scope donde fue creada, incluso después de que esa función exterior haya terminado de ejecutarse.
Lo más fácil es verlo con un ejemplo real: una factoría de contadores.
// nombre: el héroe al que pertenece este contador
function crearContadorPartidas(nombre) {
// esta variable es privada: solo existe en este scope de función
let jugadas = 0;
// devuelve una función interna, no un valor
return function () {
// accede a jugadas del scope exterior — eso es el closure
jugadas += 1;
// imprime el estado actual
console.log(`${nombre} lleva ${jugadas} partidas esta sesión`);
};
}
// Cada llamada crea un scope independiente con su propia jugadas.
// jugadas = 0 para Tracer
const contadorTracer = crearContadorPartidas('Tracer');
// jugadas = 0 para Mercy, independiente
const contadorMercy = crearContadorPartidas('Mercy');
// Tracer lleva 1 partidas esta sesión — jugadas de Tracer pasa a 1
contadorTracer();
// Tracer lleva 2 partidas esta sesión — jugadas de Tracer pasa a 2
contadorTracer();
// Mercy lleva 1 partidas esta sesión — jugadas de Mercy sigue en su propio 0+1
contadorMercy();Cuando llamas a crearContadorPartidas('Tracer'), se ejecuta la función, se crea
la variable jugadas = 0, y se devuelve la función interna. Lo importante: esa
función interna cierra sobre jugadas — la recuerda. Cada vez que llamas a
contadorTracer(), accede a esa misma jugadas y la incrementa.
contadorMercy tiene su propia copia independiente de jugadas, porque se creó
en una llamada separada a crearContadorPartidas. Los dos contadores no se
interfieren.
Para qué sirve esto en la práctica: cuando necesitas estado privado (un valor
que persiste entre llamadas pero que nadie de fuera puede modificar directamente)
sin recurrir a variables globales ni a estructuras más complejas. En los capítulos
de React, verás que los hooks como useState se apoyan en el mismo mecanismo.
Funciones puras#
Una función pura cumple dos condiciones: dado el mismo input, devuelve siempre el mismo output; y no produce efectos colaterales (no modifica variables externas, no imprime, no escribe en ningún sitio fuera de ella misma).
// Función pura: mismo input → mismo output, sin tocar nada externo.
// 78 y 120 siempre producen 0.65, sin importar qué hay fuera.
function calcularWinrate(victorias, partidas) {
// solo opera con sus parámetros y devuelve el resultado
return victorias / partidas;
}
// Función impura: lee y modifica una variable externa.
// Su resultado depende de algo ajeno a sus parámetros.
// acumulado es una variable externa que esta función modifica
let acumulado = 0;
function sumarPartidas(nuevas) {
// cambia el estado externo: efecto colateral
acumulado += nuevas;
// devuelve el nuevo valor, pero depende del estado previo de acumulado
return acumulado;
}La diferencia práctica: calcularWinrate es predecible. Puedes probarla, razonar
sobre ella y reutilizarla sin sorpresas. sumarPartidas depende del valor de
acumulado en ese momento: dos llamadas con el mismo argumento pueden dar resultados
distintos.
En el ejercicio verás esto reflejado en el tier excelente: las funciones
calcularWinrate y formatearWinrate son puras; crearContadorPartidas mantiene
estado privado con un closure porque su propósito es recordar entre llamadas, pero
ese estado es invisible desde fuera.
Pruébalo tú#
El playground muestra las tres formas de definir una función, los valores por
defecto y un closure completo. Observa que contadorTracer y contadorMercy
tienen contadores independientes aunque comparten el mismo código.
Comprueba lo que sabes#
Pregunta 1 de 5
¿Cuál es la diferencia entre una declaración de función y una expresión de función?
Tu turno#
El starter tiene los datos de tres héroes como variables sueltas. Tu tarea: escribir las funciones que calculan y formatean su winrate, y una factoría con closure para rastrear partidas. Cuando lo tengas, despliega las soluciones y fíjate en el salto de un tier al siguiente.
Ejercicio · en esta página
Winrate y contador con closure
Tienes los datos en crudo de tres héroes (nombre, rol, victorias, partidas como variables sueltas). Escribe las funciones que calculan y formatean su winrate, y una factoría con closure para rastrear partidas de sesión.
Paso 1: Que funcione
- Los tres héroes se imprimen con su winrate calculado.
- Hay al menos una función definida (aunque sea una que hace todo).
- El resultado numérico es correcto.
Paso 2: Que esté pulido
- calcularWinrate y formatearWinrate son funciones separadas con una sola responsabilidad cada una.
- fichaHeroe recibe parámetros y devuelve un string — no imprime dentro.
- No hay cálculos duplicados: si cambia la fórmula, solo se toca un sitio.
Paso 3: Que sea excelente
- Funciones puras: mismo input → mismo output, sin variables globales de apoyo.
- Valor por defecto en al menos un parámetro donde aporte sentido.
- crearContadorPartidas usa un closure real: el contador es privado e independiente por héroe.
- Un comentario explica por qué la pureza y el scope reducen la superficie de bugs.
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "funciona"
//
// Produce la salida correcta. Pero tiene problemas que se van acumulando
// a medida que el código crece:
//
// - El cálculo del winrate está copiado en fichaHeroe1, fichaHeroe2 y
// fichaHeroe3: si mañana cambia la fórmula, hay que tocar tres sitios.
// - Cada "función" imprime por dentro con console.log en lugar de devolver
// un valor: no puedes reutilizarla en otro contexto (una web, un test…).
// - Los nombres son genéricos (fichaHeroe1, fichaHeroe2…): no expresan
// intención ni indican que las tres son la misma operación.
// - Variables globales sueltas con nombres cortos (w, p) que podrían
// chocar con otra parte del código.
//
// Primer requisito cumplido: el resultado es correcto. Eso vale como OK.
// ════════════════════════════════════════════════════════════════════════════
const nombre1 = "Tracer";
const rol1 = "Daño";
const victorias1 = 78;
const partidas1 = 120;
const nombre2 = "Reinhardt";
const rol2 = "Tanque";
const victorias2 = 51;
const partidas2 = 90;
const nombre3 = "Mercy";
const rol3 = "Apoyo";
const victorias3 = 130;
const partidas3 = 200;
// El winrate se calcula tres veces, con el mismo código repetido.
function fichaHeroe1() {
// winrate en %, 1 decimal
const w = ((victorias1 / partidas1) * 100).toFixed(1);
// imprime la ficha directamente
console.log(`${nombre1} (${rol1}) — Partidas: ${partidas1} | Winrate: ${w}%`);
}
function fichaHeroe2() {
// mismo cálculo, copiado
const w = ((victorias2 / partidas2) * 100).toFixed(1);
// imprime dentro de la función
console.log(`${nombre2} (${rol2}) — Partidas: ${partidas2} | Winrate: ${w}%`);
}
function fichaHeroe3() {
// mismo cálculo, copiado de nuevo
const w = ((victorias3 / partidas3) * 100).toFixed(1);
// imprime dentro de la función
console.log(`${nombre3} (${rol3}) — Partidas: ${partidas3} | Winrate: ${w}%`);
}
// llama a la función: ejecuta el bloque y muestra la ficha de Tracer
fichaHeroe1();
// llama a la función: ejecuta el bloque y muestra la ficha de Reinhardt
fichaHeroe2();
// llama a la función: ejecuta el bloque y muestra la ficha de Mercy
fichaHeroe3(); Por qué este nivel
- Produce la salida correcta: ese es el primer requisito de cualquier solución.
- Pero el cálculo del winrate está copiado tres veces: si la fórmula cambia, hay que tocar tres sitios.
- Las funciones imprimen con console.log en lugar de devolver un valor, así que no se pueden reutilizar en otro contexto.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL MEJOR — "pulido"
//
// Una función por responsabilidad, parámetros bien elegidos, return en lugar
// de imprimir dentro. El código es reutilizable y legible.
//
// Lo que mejora sobre OK:
// - calcularWinrate y formatearWinrate son funciones pequeñas y reutilizables.
// Si mañana cambia la fórmula, se toca un solo sitio.
// - fichaHeroe recibe parámetros y devuelve un string (no imprime dentro):
// se puede usar en una web, en un test, o en otro contexto sin cambiar nada.
// - Sin variables globales de apoyo: cada función tiene su propio scope.
// - Nombres claros que expresan intención.
//
// Todavía hay margen: los valores por defecto y el closure del bonus
// son el siguiente paso.
// ════════════════════════════════════════════════════════════════════════════
const nombre1 = "Tracer";
const rol1 = "Daño";
const victorias1 = 78;
const partidas1 = 120;
const nombre2 = "Reinhardt";
const rol2 = "Tanque";
const victorias2 = 51;
const partidas2 = 90;
const nombre3 = "Mercy";
const rol3 = "Apoyo";
const victorias3 = 130;
const partidas3 = 200;
// Una responsabilidad: calcular el winrate como número.
function calcularWinrate(victorias, partidas) {
// devuelve un número entre 0 y 1
return victorias / partidas;
}
// Una responsabilidad: convertir el winrate en texto formateado.
function formatearWinrate(winrate) {
// multiplica por 100 y redondea a 1 decimal
return `${(winrate * 100).toFixed(1)}%`;
}
// Una responsabilidad: construir la ficha completa — devuelve, no imprime.
function fichaHeroe(nombre, rol, victorias, partidas) {
// calcula el ratio numérico
const winrate = calcularWinrate(victorias, partidas);
// lo convierte en texto
const winratePct = formatearWinrate(winrate);
// devuelve la ficha completa
return `${nombre} (${rol}) — Partidas: ${partidas} | Winrate: ${winratePct}`;
}
// imprime la ficha de Tracer
console.log(fichaHeroe(nombre1, rol1, victorias1, partidas1));
// imprime la ficha de Reinhardt
console.log(fichaHeroe(nombre2, rol2, victorias2, partidas2));
// imprime la ficha de Mercy
console.log(fichaHeroe(nombre3, rol3, victorias3, partidas3)); Por qué es mejor que el anterior
- Una función por responsabilidad: calcularWinrate, formatearWinrate y fichaHeroe hacen una cosa cada una.
- fichaHeroe devuelve un string — quien la llama decide qué hacer con él (imprimir, guardar, enviar a una API).
- El cálculo está en un solo sitio: cambiar la fórmula es tocar una línea.
// ════════════════════════════════════════════════════════════════════════════
// NIVEL EXCELENTE — "óptimo"
//
// Funciones puras, valores por defecto y un closure con criterio.
//
// Por qué importa cada decisión:
//
// PUREZA: calcularWinrate y formatearWinrate son funciones puras — mismo
// input, mismo output, sin tocar nada de fuera. Eso las hace predecibles,
// testeables y reutilizables sin sorpresas. Una función que modifica variables
// externas o imprime por dentro no puede reutilizarse sin efectos colaterales.
//
// VALORES POR DEFECTO: fichaHeroe tiene `rol = 'Sin rol'`. No rompe si el
// dato no llega — algo frecuente cuando los datos vienen de una API externa.
// El valor por defecto documenta qué se espera y qué es opcional.
//
// CLOSURE: crearContadorPartidas devuelve una función que "recuerda" el
// contador porque cierra sobre la variable `jugadas`, que vive en el scope
// de crearContadorPartidas, no en el global. Cada héroe tiene su propio
// contador independiente. Sin closure, necesitarías variables globales o
// un objeto — más código, más superficie para errores.
//
// SCOPE DE BLOQUE: `jugadas` no existe fuera de crearContadorPartidas.
// Nadie puede pisarla por accidente desde otro sitio del código.
// ════════════════════════════════════════════════════════════════════════════
const nombre1 = "Tracer";
const rol1 = "Daño";
const victorias1 = 78;
const partidas1 = 120;
const nombre2 = "Reinhardt";
const rol2 = "Tanque";
const victorias2 = 51;
const partidas2 = 90;
const nombre3 = "Mercy";
const rol3 = "Apoyo";
const victorias3 = 130;
const partidas3 = 200;
// Función pura: mismo input → mismo output, sin efectos colaterales.
// Arrow function de una sola expresión: victorias / partidas se devuelve automáticamente.
const calcularWinrate = (victorias, partidas) => victorias / partidas;
// Función pura: transforma un número en texto, sin tocar nada externo.
// winrate * 100 convierte el ratio a porcentaje; toFixed(1) redondea a 1 decimal.
const formatearWinrate = (winrate) => `${(winrate * 100).toFixed(1)}%`;
// Parámetro por defecto: si no se pasa `rol`, usa 'Sin rol'.
// Devuelve el string — quien llama decide si imprimirlo, guardarlo o enviarlo.
// rol = 'Sin rol' es el valor por defecto
function fichaHeroe(nombre, victorias, partidas, rol = "Sin rol") {
// encadena las dos funciones puras
const winratePct = formatearWinrate(calcularWinrate(victorias, partidas));
// devuelve la ficha completa
return `${nombre} (${rol}) — Partidas: ${partidas} | Winrate: ${winratePct}`;
}
// imprime la ficha de Tracer
console.log(fichaHeroe(nombre1, victorias1, partidas1, rol1));
// imprime la ficha de Reinhardt
console.log(fichaHeroe(nombre2, victorias2, partidas2, rol2));
// imprime la ficha de Mercy
console.log(fichaHeroe(nombre3, victorias3, partidas3, rol3));
// Closure: crearContadorPartidas cierra sobre `jugadas`.
// Cada héroe tiene su propio contador — no hay variable global que pisar.
// nombre: identifica a qué héroe pertenece este contador
function crearContadorPartidas(nombre) {
// privado: solo existe dentro de esta llamada a crearContadorPartidas
let jugadas = 0;
// Esta función interna recuerda `jugadas` aunque crearContadorPartidas
// ya haya terminado de ejecutarse — eso es el closure.
return function () {
// incrementa el contador privado de este héroe
jugadas += 1;
// muestra el estado
console.log(`${nombre} lleva ${jugadas} partidas registradas esta sesión`);
};
}
// Cada llamada crea un scope propio: contadorTracer y contadorMercy no comparten jugadas.
// jugadas = 0 para Tracer
const contadorTracer = crearContadorPartidas("Tracer");
// jugadas = 0 para Mercy, independiente
const contadorMercy = crearContadorPartidas("Mercy");
// Tracer lleva 1 partidas registradas esta sesión
contadorTracer();
// Tracer lleva 2 partidas registradas esta sesión
contadorTracer();
// Mercy lleva 1 partidas registradas esta sesión — su jugadas no es la de Tracer
contadorMercy();
// Tracer lleva 3 partidas registradas esta sesión
contadorTracer();
// Mercy sigue en 1: cada closure tiene su propia copia de `jugadas`. Por qué es mejor que el anterior
- Funciones puras: mismo input produce siempre el mismo output, sin estado oculto ni efectos colaterales. Eso las hace predecibles y testeables.
- El valor por defecto en fichaHeroe documenta qué es opcional y evita errores cuando el dato no llega.
- El closure de crearContadorPartidas mantiene un contador privado por héroe: sin variables globales que pisar, sin riesgo de que un contador interfiera con otro.