Funciones y closures

Lección 4 de 8. Las funciones son el corazón de JavaScript: se guardan en variables, se pasan como argumento y recuerdan el sitio donde nacieron. Esta lección va de las tres formas de escribirlas, de sus parámetros, de dónde existe cada variable y de los closures, que es lo que más cuesta al principio y lo que más se usa después.

Tres formas de escribir una función

// 1. Declaración
function doble(n) {
  return n * 2;
}

// 2. Expresión: una función guardada en una constante
const triple = function (n) {
  return n * 3;
};

// 3. Función flecha
const cuadrado = (n) => n * n;

console.log(doble(5), triple(5), cuadrado(5));
salida10 15 25

Las tres hacen lo mismo aquí. La flecha es la más corta: si el cuerpo es una sola expresión, se omiten las llaves y el return, y si tiene un único parámetro, incluso los paréntesis (n => n * n). Es la que verás en casi todo el código moderno, sobre todo como argumento de otras funciones.

DeclaraciónExpresiónFlecha
Se puede llamar antes de su líneasínono
Tiene su propio thissísíno: usa el de fuera
Tiene argumentssísíno
Uso típicofunciones con nombre de un módulorara vez hoycallbacks y funciones cortas

Lo de this tiene su propia explicación en la lección 6. Lo de llamar antes de la línea se llama hoisting y merece un ejemplo:

console.log(doble(4));        // funciona: las declaraciones se «elevan»
console.log(triple(4));       // falla

function doble(n) { return n * 2; }
const triple = (n) => n * 3;
salida8
salidaReferenceError: Cannot access 'triple' before initialization

Antes de ejecutar, JavaScript registra todas las declaraciones function del archivo, así que se pueden usar desde arriba. Una función guardada en una const no existe hasta que se llega a su línea, y antes de eso el nombre está en la llamada zona muerta temporal: existe, pero tocarlo es un error. Lo veremos otra vez más abajo con las variables.

Parámetros

Los que faltan y los que sobran

JavaScript no comprueba cuántos argumentos recibe una función. Los que faltan valen undefined y los que sobran se ignoran, sin error:

function saluda(nombre, saludo) {
  return `${saludo}, ${nombre}`;
}

console.log(saluda("Ana"));
console.log(saluda("Ana", "Hola", "esto sobra"));
salidaundefined, Ana
Hola, Ana

Valores por omisión

function saluda(nombre, saludo = "Hola") {
  return `${saludo}, ${nombre}`;
}
console.log(saluda("Ana"));
console.log(saluda("Luis", "Buenas"));
console.log(saluda("Eva", undefined));    // undefined activa el valor por omisión
console.log(saluda("Leo", null));         // null no

function agrega(x, lista = []) {
  lista.push(x);
  return lista;
}
console.log(agrega(1), agrega(2));
salidaHola, Ana
Buenas, Luis
Hola, Eva
null, Leo
[ 1 ] [ 2 ]

El valor por omisión solo se usa cuando el argumento es undefined, falte o se pase así a propósito; con null no. Y se calcula en cada llamada: la lista vacía de agrega es nueva cada vez, así que la segunda llamada devuelve [ 2 ] y no [ 1, 2 ]. Si vienes de Python, es justo al revés de lo que allí pasa con los valores por omisión mutables.

Resto y propagación: los tres puntos

Los mismos tres puntos hacen dos cosas opuestas según dónde estén. En los parámetros, recogen los argumentos que queden en un array (parámetro resto); en una llamada, reparten un array en argumentos sueltos (propagación):

function media(...numeros) {
  const suma = numeros.reduce((total, n) => total + n, 0);
  return suma / numeros.length;
}
console.log(media(4, 8, 6));

const notas = [7, 9.5, 6];
console.log(media(...notas));
console.log(Math.max(...notas));
salida6
7.5
9.5

El parámetro resto tiene que ser el último. En código antiguo verás arguments, un objeto parecido a un array con todos los argumentos; las flechas no lo tienen y el parámetro resto lo sustituye con ventaja, porque es un array de verdad.

Ámbito: dónde existe cada variable

Una variable declarada con let o const existe solo dentro del bloque, el par de llaves, donde se declara. Con var, en cambio, el bloque no cuenta: existe en toda la función.

if (true) {
  var fuera = "var se escapa del bloque";
  let dentro = "let no";
}
console.log(fuera);
console.log(dentro);
salidavar se escapa del bloque
salidaReferenceError: dentro is not defined

Y la zona muerta temporal que vimos con triple: una variable con let o const no se puede usar antes de su línea, ni siquiera para leerla. Con var sí se puede, y da undefined, que es peor, porque el programa sigue con un valor que nadie ha puesto:

console.log("var antes de su línea:", precio);
var precio = 10;
salidavar antes de su línea: undefined
console.log("let antes de su línea:", precio);
let precio = 10;
salidaReferenceError: Cannot access 'precio' before initialization
Por eso este curso no usa var. Sus reglas de ámbito son las que se diseñaron en 1995 y causan errores que let y const evitan. Usa const por omisión y let solo cuando vayas a reasignar la variable.

Closures: funciones que recuerdan

Una función puede leer las variables del sitio donde se escribió, no solo las suyas. Y si esa función sale de allí, por ejemplo porque otra la devuelve, se lleva esas variables consigo. A esa combinación de función y variables recordadas se le llama closure (clausura):

function crearContador() {
  let cuenta = 0;
  return function () {
    cuenta++;
    return cuenta;
  };
}

const a = crearContador();
const b = crearContador();
console.log(a(), a(), a(), "|", b());
salida1 2 3 | 1

La función que devuelve crearContador conserva acceso a la variable cuenta de su ámbitoUn closure: la función devuelta se lleva su ámbitoámbito globalconst a = crearContador();llamada a crearContador()cuenta = 0function () {cuenta++;return cuenta; }la usaa (la función devuelta)sigue viendo cuenta aunquecrearContador ya terminóa() a() a() da 1, 2, 3Cada llamada a crearContador()crea un ámbito nuevo, con supropia cuenta: b = crearContador()empieza otra vez en 0.

Cuando crearContador termina, su variable cuenta no desaparece: la función devuelta la sigue usando. Cada llamada a crearContador crea una cuenta nueva. Esquema: decodigo.com.

Es la forma más sencilla de tener estado privado: nadie de fuera puede tocar cuenta, solo la función que la usa. a y b tienen cada una la suya, por eso b() empieza en 1. Los closures están detrás de los manejadores de eventos, de los temporizadores y de casi cualquier callback que use una variable de fuera.

El clásico: un bucle, un temporizador y var

Este ejemplo junta todo lo anterior y es una pregunta típica de entrevista. Cada vuelta programa una función que se ejecutará más tarde y que imprime i:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log("var:", i), 0);
}
salidavar: 3
var: 3
var: 3
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log("let:", i), 0);
}
salidalet: 0
let: 1
let: 2

Con var, los tres callbacks comparten una sola variable i que acaba en 3; con let, cada vuelta tiene su propia isetTimeout dentro de un bucle: var frente a letfor (var i = 0; i < 3; i++)callback 1callback 2callback 3i = 3una sola i compartida: imprime 3 3 3for (let i = 0; i < 3; i++)callback 1callback 2callback 3i = 0i = 1i = 2una i nueva en cada vuelta: imprime 0 1 2

Con var solo hay una i para todo el bucle, y cuando los callbacks se ejecutan ya vale 3. Con let, cada vuelta crea una i nueva y cada callback recuerda la suya. Esquema: decodigo.com.

Los callbacks se ejecutan cuando el bucle ya ha terminado (en la lección 7 se ve por qué). Con var, los tres recuerdan la misma variable, que para entonces vale 3. Con let, el lenguaje crea una variable nueva en cada vuelta, y cada closure se queda con la suya.

Funciones que reciben y devuelven funciones

En JavaScript, una función es un valor como cualquier otro: se guarda en una variable, se pasa como argumento y se devuelve. A las funciones que reciben o devuelven otras se las llama de orden superior, y son el pan de cada día:

// recibe una función
function repetir(veces, accion) {
  for (let i = 1; i <= veces; i++) accion(i);
}
repetir(3, (n) => console.log("vuelta", n));

// devuelve una función
const multiplicarPor = (factor) => (n) => n * factor;
const iva = multiplicarPor(1.16);
console.log(iva(100).toFixed(2));

// y las dos cosas a la vez, con los métodos de los arrays
console.log([1, 2, 3].map(multiplicarPor(10)));
salidavuelta 1
vuelta 2
vuelta 3
116.00
[ 10, 20, 30 ]

multiplicarPor es una flecha que devuelve otra flecha, y la de dentro recuerda factor: otro closure. map, filter y compañía, que se ven en la lección 5, reciben funciones así.

Dos errores frecuentes

Llamar a la función en vez de pasarla

function saluda() {
  console.log("hola");
}
setTimeout(saluda(), 100);
salidahola
salidaTypeError [ERR_INVALID_ARG_TYPE]: The "callback" argument must be of type function. Received undefined

Con los paréntesis, saluda() se ejecuta en el acto (por eso sale «hola» enseguida) y lo que recibe setTimeout es lo que devuelve, undefined. Hay que pasar la función sin llamarla: setTimeout(saluda, 100). En el navegador, con addEventListener("click", saluda()), el error es más traicionero: no hay mensaje, simplemente el clic no hace nada.

Una flecha que devuelve un objeto

const crearMal = (nombre) => { nombre: nombre };
const crearBien = (nombre) => ({ nombre: nombre });

console.log(crearMal("Ana"));
console.log(crearBien("Ana"));
salidaundefined
{ nombre: 'Ana' }

Tras la flecha, una llave abre un bloque, no un objeto. JavaScript lee nombre: como una etiqueta (las de la lección 3) y la función no devuelve nada. Para devolver un objeto literal, entre paréntesis.

Todo el código de esta lección se ejecutó con Node.js 24 en un contenedor limpio antes de publicarla; las salidas y los errores están copiados de esa ejecución.