learning-front

Nivel 2 · JavaScript: fundamentos del lenguaje

Funciones, scope y closures

Declaración, expresión, arrow functions, parámetros, retorno y el alcance de las variables.

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:

javascript
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.

javascript
// 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#

javascript
// 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#

javascript
// 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 de this. 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:

javascript
// 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:

javascript
// 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.

javascript
// 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.

javascript
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í.

javascript
// 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.

javascript
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.

javascript
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.

javascript
// 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).

javascript
// 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.
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.