Si hay un tema de JavaScript que aparece en casi todas las entrevistas de frontend, son los closures. Y no se pueden entender bien sin entender primero las funciones. Así que empecemos por ahí.

Las funciones son valores

Una función es un bloque de código reutilizable que hace una tarea. En JavaScript, las funciones también son objetos de primera clase (first-class objects): las puedes guardar en variables, pasarlas como argumentos, regresarlas desde otras funciones y hasta agregarles propiedades.

Esa sola idea es la que hace posibles los callbacks, los event handlers, los componentes de React y los closures.

Anatomía de una función en JavaScript

Hay varias formas de crear una función, y en una entrevista las diferencias importan.

Declaración de función

La forma clásica: la palabra function, un nombre, parámetros y un cuerpo.

function suma(a, b) { return a + b; } suma(2, 4); // 6 

Las declaraciones de función hacen hoisting completo, así que las puedes llamar antes de la línea donde están escritas.

Expresión de función

Una función creada como parte de una expresión, normalmente asignada a una variable.

const suma = function (a, b) { return a + b; }; suma(2, 4); // 6 

Se ve casi igual, pero hay una diferencia clave: solo la variable hace hoisting, no la función. Si la llamas antes de esa línea, falla.

saludar(); // ✅ funciona: es una declaración despedir(); // ❌ ReferenceError (con const) o TypeError (con var) function saludar() { console.log("hola"); } const despedir = function () { console.log("adiós"); }; 

Arrow functions

Llegaron con ES6 (2015). Tienen una sintaxis más corta, y si el cuerpo es una sola expresión, la regresan automáticamente.

const suma = (a, b) => a + b; suma(2, 4); // 6 // Para regresar un objeto literal, envuélvelo en paréntesis const crearUsuario = (nombre) => ({ nombre }); 

Pero las arrow functions no son solo una forma más corta de escribir. Se comportan distinto:

  • No tienen su propio this. Usan el this del código que las rodea. Son geniales para callbacks y malas para métodos de objetos.
  • No tienen el objeto arguments. En su lugar se usan rest parameters (...args).
  • No se pueden usar con new. new (() => {}) lanza TypeError: ... is not a constructor.
const temporizador = { segundos: 0, iniciar() { setInterval(() => { this.segundos++; // `this` sigue siendo `temporizador`, gracias a la arrow function }, 1000); }, }; 

Con una function normal dentro de setInterval, this ya no apuntaría a temporizador. Es una pregunta de seguimiento muy común.

IIFE (Immediately Invoked Function Expression)

Una función que se ejecuta en cuanto se define.

(function () { const secreto = "solo visible aquí dentro"; console.log("Hola mundo"); })(); 

Antes de los módulos de ES y de let/const, las IIFE eran la forma principal de crear un scope privado y no ensuciar el global. Todavía las vas a ver en código viejo y en preguntas de entrevista.

Funciones constructoras y clases

Una función constructora crea objetos cuando la llamas con new.

function Usuario(nombre, apellido) { this.nombre = nombre; this.apellido = apellido; } const usuario = new Usuario("Jon", "Snow"); // { nombre: "Jon", apellido: "Snow" } 

Cuando usas new, JavaScript:

  1. crea un objeto vacío,
  2. lo enlaza con el prototype del constructor,
  3. ejecuta la función con this apuntando a ese objeto,
  4. regresa el objeto (a menos que la función regrese explícitamente otro objeto).

Hoy normalmente escribirías lo mismo con class, que en el fondo es sintaxis sobre el mismo mecanismo:

class Usuario { constructor(nombre, apellido) { this.nombre = nombre; this.apellido = apellido; } } 

Respuesta para la entrevista: "En JavaScript las funciones son objetos de primera clase. Las declaraciones de función hacen hoisting completo; las expresiones no. Las arrow functions son más cortas, no tienen su propio this ni arguments, y no se pueden usar como constructoras. Las funciones constructoras y las clases crean objetos con new, que asigna this a un objeto nuevo enlazado al prototype."

Closures

La definición

Un closure es una función que recuerda las variables del scope donde se creó, incluso después de que ese scope terminó de ejecutarse.

Toda función en JavaScript forma un closure. No es una sintaxis especial; es una consecuencia de que las funciones sean valores y de la scope chain.

El ejemplo

En la primera versión de este post, mi ejemplo tenía un bug: llamaba a la función de afuera y esperaba que imprimiera. Esta es la versión corregida:

function decirHola() { const saludo = "Hola"; function interna() { console.log(saludo); } return interna; } const hola = decirHola(); // decirHola ya terminó... hola(); // ...pero interna todavía recuerda `saludo`: "Hola" 

decirHola ya regresó, así que en teoría saludo debería haber desaparecido. Pero interna guarda una referencia al scope donde se creó, así que saludo sigue vivo mientras exista interna.

Para qué sirve: estado privado

Los closures te permiten crear datos a los que no se puede acceder desde afuera.

function crearContador() { let cuenta = 0; return { incrementar() { cuenta++; return cuenta; }, obtener() { return cuenta; }, }; } const contador = crearContador(); contador.incrementar(); contador.incrementar(); contador.obtener(); // 2 contador.cuenta; // undefined: no hay forma de tocar `cuenta` directamente 

La pregunta clásica de entrevista

Seguramente te va a tocar esta:

for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i)); } // 3, 3, 3 

¿Por qué 3, 3, 3 y no 0, 1, 2? Porque var tiene scope de función, así que hay una sola i. Los tres callbacks hacen closure sobre la misma variable, y cuando por fin se ejecutan, el loop ya terminó e i vale 3.

La solución es let, que crea una variable nueva en cada iteración:

for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i)); } // 0, 1, 2 

Closures en la vida real: React

Si usas React, usas closures todos los días. Cada render crea funciones nuevas que hacen closure sobre las props y el estado de ese render. De ahí viene el famoso bug del "stale closure" (closure obsoleto):

function Contador() { const [cuenta, setCuenta] = useState(0); useEffect(() => { const id = setInterval(() => { setCuenta(cuenta + 1); // siempre ve la `cuenta` del primer render }, 1000); return () => clearInterval(id); }, []); return <p>{cuenta}</p>; // se queda atorado en 1 } 

El callback del intervalo hizo closure sobre cuenta = 0 y nunca ve las actualizaciones. La solución es la actualización funcional, que no depende del valor guardado en el closure:

setCuenta((c) => c + 1); 

Mencionar esto en una entrevista demuestra que entiendes los closures más allá de la definición de libro.

Respuesta para la entrevista: "Un closure es una función que conserva acceso a las variables del scope donde se creó, incluso después de que ese scope terminó. Se usa para estado privado, fábricas de funciones, callbacks y memoización. Un ejemplo clásico es el loop con var y setTimeout que imprime el mismo valor tres veces; let lo resuelve porque crea una variable nueva por iteración. En React, los stale closures en efectos son un bug común causado por el mismo mecanismo."

Checklist rápida

  • [ ] ¿Puedes explicar por qué las funciones son "objetos de primera clase"?
  • [ ] ¿Sabes qué formas de función hacen hoisting y cuáles no?
  • [ ] ¿Puedes nombrar tres diferencias entre arrow functions y funciones normales?
  • [ ] ¿Puedes explicar qué hace new, paso a paso?
  • [ ] ¿Puedes definir un closure en una frase y dar un ejemplo?
  • [ ] ¿Puedes explicar la salida del loop con var y setTimeout?

Anterior en la serie: variables, tipos de dato, scope y hoisting.
Siguiente: JavaScript asíncrono: callbacks, promesas y async/await.

¿Cuál fue tu momento "¡ahhh, ya entendí!" con los closures? Cuéntame en los comentarios 👇