Funciones, arrow functions y closures
Lección 3 de 43 · El lenguaje: de JavaScript a TypeScript estricto

De JavaScript a TypeScript estrictoLección 3/4
Temario del curso
De JavaScript a TypeScript estricto
Lección 3 de 4
CursoDe JavaScript a TypeScript estricto · Lección 3 de 4
Hay tres formas de declarar una función y ninguna es intercambiable con las otras dos. Se diferencian en cuándo existen, en cómo tratan this y en si pueden usarse como constructor.
Esta lección recorre esas diferencias, explica qué es una closure y termina en el patrón de función que devuelve función, que es la base de los handlers parametrizados y de los middleware que verás más adelante en el curso.
Nota sobre la terminología: en este artículo, invocar y llamar significan lo mismo. Y ámbito siempre se refiere al ámbito léxico, es decir, la región del código donde una variable es visible.
Las tres formas de declarar una función
Declaración de función
ts
function sumar(a: number, b: number): number {
return a + b;
}Definición — Elevación (hoisting): Mecanismo por el que ciertas declaraciones se registran antes de que se ejecute cualquier línea del archivo. Una declaración de función se eleva completa, así que puedes llamarla desde arriba aunque esté escrita abajo.
ts
console.log(sumar(1, 2)); // 3, aunque `sumar` se declare después
function sumar(a: number, b: number): number {
return a + b;
}Eso te permite escribir un archivo con las funciones principales arriba y las auxiliares abajo, sin pelearte con el orden.
Expresión de función
ts
const sumar = function (a: number, b: number): number {
return a + b;
};Aquí la función es un valor asignado a una variable, así que se rige por las reglas de const. La variable existe pero no está inicializada hasta que se ejecuta esa línea, y llamarla antes lanza un error de acceso antes de la inicialización.
Arrow function
ts
const sumar = (a: number, b: number): number => a + b;También es una expresión, así que tampoco se eleva. Con un cuerpo de una sola expresión, el return es implícito.
Su diferencia importante no es la brevedad, sino cómo trata this. A eso vamos en la sección siguiente.
Ten en cuenta: una arrow function no puede usarse con
new, no tiene el objetoargumentsy no puede ser un generador. En la práctica esas restricciones casi nunca estorban: cuando lo hacen, suelen indicar que ese sitio pedía una función normal o una clase.
Cuál usar
Las expresiones de función clásicas casi no se usan ya: donde antes iba una, hoy va una arrow. La decisión real es entre declaración y arrow:
UsaParaDeclaraciónFunciones con nombre, exportadas, que forman la interfaz del móduloArrowFunciones que se pasan como argumento, funciones que devuelven funciones, y cualquiera que necesite el this de su entorno
this léxico y los errores al perder el contexto
Aquí es donde más tiempo se pierde depurando, así que conviene construirlo desde lo básico.
Definición — this: Referencia al objeto sobre el que se está ejecutando una función. En una función normal, su valor no depende de dónde se escribió la función, sino de cómo se llamó, y se decide en cada invocación.
El caso mínimo
ts
const contador = {
valor: 0,
incrementar: function () {
this.valor += 1; // `this` es quien haya invocado
},
};
contador.incrementar(); // `this` es `contador` → funcionaAhora la misma función, sacada de su objeto:
ts
const fn = contador.incrementar;
fn(); // `this` ya no es `contador`Al asignar contador.incrementar a una variable copias solo la función, no su relación con el objeto. Al llamarla sin punto delante, this no apunta a contador. En modo estricto, que es como se ejecutan los módulos ES, vale undefined, y la línea falla al intentar leer .valor.
El caso que aparece en código real
El mismo problema surge siempre que entregas la función a otro para que la llame más tarde:
ts
class Servicio {
private nombre = 'colas';
procesar(): void {
console.log(this.nombre);
}
registrar(items: string[]): void {
items.forEach(this.procesar); // ← quien llama es forEach, no la instancia
}
}forEach invoca la función que recibe sin ningún objeto delante, así que dentro de procesar el this no es la instancia.
La solución: this léxico
Definición — this léxico: Una arrow function no tiene this propio. Usa el del ámbito donde fue escrita, y esa decisión se toma al definirla, no al llamarla.
ts
registrar(items: string[]): void {
items.forEach(() => this.procesar()); // `this` es el del método → la instancia
}O declarando el método directamente como campo con arrow, cuando esa función se va a pasar por ahí constantemente:
ts
class Servicio {
private nombre = 'colas';
procesar = (): void => {
console.log(this.nombre); // `this` fijado a la instancia al construirla
};
}Existen también bind, call y apply para fijar this de forma explícita, como en this.procesar.bind(this). En código nuevo, la arrow function cubre casi todos los casos con menos ruido.
El caso inverso también sorprende: dentro de un método definido como arrow en un objeto literal,
thisno es ese objeto, sino el del ámbito exterior. En un objeto literal, un método que necesitethisdebe ser una función normal.
El atajo para recordarlo
Una sola pregunta: ¿quién llama a esta función?
-
Si la respuesta es "otro código, más tarde, y no sé con qué delante" → necesitas una arrow o un
bind. -
Si es "siempre se llama con su objeto delante" → una función normal está bien.
Closures: la función que recuerda dónde nació
Definición — Closure: Una función que conserva acceso a las variables del ámbito donde fue creada, incluso después de que ese ámbito haya terminado. No es una construcción que haya que activar: es una consecuencia de cómo funcionan los ámbitos en JavaScript.
El ejemplo mínimo
ts
function crearContador() {
let cuenta = 0; // variable local de crearContador
return function () {
cuenta += 1; // sigue viéndola después de que la función terminó
return cuenta;
};
}
const contar = crearContador();
contar(); // 1
contar(); // 2Cuando crearContador() termina, cuenta debería desaparecer. No lo hace: la función devuelta mantiene viva una referencia a ella. Eso es la closure.
Las dos propiedades que hay que interiorizar
Cada llamada a la función externa crea un ámbito nuevo, con su propia copia de las variables:
ts
const a = crearContador();
const b = crearContador();
a(); // 1
b(); // 1 ← estado independienteLo que la closure captura es la variable, no su valor en el momento de crearse. Si cambia después, la closure ve el valor nuevo:
ts
let mensaje = 'inicial';
const mostrar = () => console.log(mensaje);
mensaje = 'cambiado';
mostrar(); // 'cambiado'Para qué sirve
-
Estado privado de verdad. La variable capturada no es accesible desde fuera por ningún medio. No hay una propiedad que leer ni una convención de nombre que respetar.
-
Configuración fijada una sola vez. La función se crea ya "sabiendo" ciertos datos, y quien la usa después no tiene que volver a pasárselos. Es justo lo que aprovecha el patrón siguiente.
Ten en cuenta: una closure mantiene vivo lo que captura mientras la función exista. Capturar un objeto grande en una función guardada a nivel de módulo lo deja en memoria durante toda la vida del proceso. Captura solo el dato que hace falta, no el contenedor entero.
El patrón de fábrica de handlers
Aquí se juntan las dos piezas anteriores: una arrow function que devuelve otra arrow function, aprovechando la closure para recordar la configuración.
El problema que resuelve
Imagina varias rutas HTTP que hacen exactamente lo mismo, encolar el cuerpo de la petición, y solo se diferencian en la acción y en la cola de destino.
Sin este patrón tienes dos salidas, y ninguna es buena. Duplicar el handler tantas veces como rutas haya, o pasar la configuración en cada llamada, cosa que no puedes hacer porque el framework llama al handler con (req, res) y nada más.
La solución
La fábrica invierte el orden. Primero le das la configuración, y lo que devuelve es un handler ya listo con la firma que el framework espera:
ts
const crearHandler = (accion: string, url: string) =>
async (req: Request, res: Response) => {
await sendToQueue({ accion, ...req.body }, url); // recuerda accion y url
res.status(202).json({ ok: true });
};Pieza a pieza:
-
crearHandlerrecibe la configuración y no hace nada más que devolver otra función. -
La función devuelta tiene exactamente la firma
(req, res)que espera el framework, así que se registra tal cual. -
Dentro de ella,
accionyurlsiguen accesibles gracias a la closure, aunquecrearHandlerterminó hace rato.
En el registro de rutas queda así:
ts
app.post('/orders', crearHandler('CREATE_ORDER', ORDERS_URL));
app.post('/invoices', crearHandler('CREATE_INVOICE', INVOICES_URL));Cada llamada a crearHandler produce un handler distinto, con su propia configuración capturada. El comportamiento se escribió una sola vez.
Nombrar el tipo de la función devuelta
ts
type Handler = (req: Request, res: Response) => Promise<void>;
const crearHandler = (accion: string, url: string): Handler =>
async (req, res) => { /* req y res ya vienen tipados */ };Definición — Tipado contextual: Mecanismo por el que el compilador deduce el tipo de los parámetros de una función a partir del tipo declarado en el sitio donde esa función se coloca.
Al anotar el retorno como Handler, los parámetros de la función interna ya no necesitan anotación: el compilador los deduce de ese tipo. Es más útil de lo que parece, porque elimina la duplicación entre la firma declarada y la implementación.
Adelanto: este mismo molde, una función que recibe configuración y devuelve la función que el framework espera, reaparece más adelante en el curso aplicado a middleware de validación, a envoltorios con reintentos y a instrumentación. Reconocer la forma ahorra entenderla otra vez cada vez que aparece.
Ejercicios
Ejercicio 1: predecir el resultado
Sin ejecutarlo, indica qué imprime cada llamada y por qué.
Contexto: el código está en un módulo ES, es decir, en modo estricto. objeto.normal es una función normal y objeto.flecha es una arrow declarada dentro del literal.
ts
const objeto = {
valor: 10,
normal: function () { return this?.valor; },
flecha: () => this?.valor,
};
const suelta = objeto.normal;
objeto.normal();
objeto.flecha();
suelta();Ejercicio 2: arreglar el this perdido
Corrige esta clase para que registrar funcione, sin cambiar la firma pública de ningún método. Ofrece dos soluciones distintas.
ts
class Notificador {
private prefijo = '[app]';
imprimir(mensaje: string): void {
console.log(this.prefijo, mensaje);
}
registrar(mensajes: string[]): void {
mensajes.forEach(this.imprimir);
}
}Ejercicio 3: contador con estado privado
Escribe una función crearLimitador(maximo: number) que devuelva una función sin parámetros. Cada llamada a la función devuelta suma uno a un contador interno y devuelve true mientras no se supere maximo, y false a partir de ahí.
Condición: el contador no debe ser accesible desde fuera de ninguna forma.
Ejercicio 4: una fábrica propia
Escribe una fábrica conPrefijo(prefijo: string) que devuelva una función (mensaje: string) => string, la cual antepone el prefijo al mensaje. Después tipa el retorno de la fábrica con un alias y comprueba que la función interna ya no necesita anotar su parámetro.
Pista: es el mismo molde del patrón de handlers, con una configuración más simple.
Soluciones
Solución 1
LlamadaImprimePor quéobjeto.normal()10Se llama con objeto delante, así que this es objetoobjeto.flecha()undefinedUna arrow toma el this del ámbito exterior, que en un módulo es undefinedsuelta()undefinedLa función se copió sin su objeto; en modo estricto this es undefined
El segundo caso es el inverso que suele sorprender: dentro de un objeto literal, una arrow no apunta al objeto que la contiene.
Solución 2
Primera solución, envolver en una arrow:
ts
registrar(mensajes: string[]): void {
mensajes.forEach(mensaje => this.imprimir(mensaje));
}Segunda solución, declarar el método como campo con arrow:
ts
class Notificador {
private prefijo = '[app]';
imprimir = (mensaje: string): void => {
console.log(this.prefijo, mensaje);
};
registrar(mensajes: string[]): void {
mensajes.forEach(this.imprimir); // ahora sí funciona
}
}Una tercera vía sería mensajes.forEach(this.imprimir.bind(this)), equivalente pero más ruidosa.
Solución 3
ts
function crearLimitador(maximo: number): () => boolean {
let usados = 0; // capturada por la closure, invisible desde fuera
return () => {
usados += 1;
return usados <= maximo;
};
}
const limite = crearLimitador(2);
limite(); // true
limite(); // true
limite(); // falseusados no es una propiedad de ningún objeto ni tiene ningún nombre accesible desde fuera. La única forma de tocarla es llamar a la función devuelta.
Solución 4
ts
type Prefijador = (mensaje: string) => string;
const conPrefijo = (prefijo: string): Prefijador =>
mensaje => `${prefijo} ${mensaje}`;
const log = conPrefijo('[app]');
log('arrancando'); // '[app] arrancando'Al anotar el retorno como Prefijador, el parámetro mensaje de la función interna ya viene tipado como string por tipado contextual. Si quitas la anotación del retorno, el compilador exige anotar mensaje a mano.
Conclusión
La elección entre declaración y arrow no es de estilo. Una arrow no tiene this propio, y eso la convierte en la opción correcta cada vez que entregas una función a otro código para que la llame más tarde.
Sobre esa base, la closure habilita el patrón que más se repite en un servicio real: una fábrica que recibe configuración una sola vez y devuelve una función ya lista con la firma que el entorno espera.
Artículos relacionados

Los pronombres personales sujeto y por qué en inglés nunca se omiten
Antes de conjugar ningún tiempo verbal hay que resolver una pregunta previa: quién hace la acción. El español permite callar esa información porque la terminación del verbo ya la lleva dentro; el inglés no lo permite casi nunca. Esta lección presenta los siete pronombres sujeto y explica por qué la omisión, que en español es elegante, en inglés produce una frase incorrecta. Nota sobre la terminología: en este artículo sujeto significa la palabra o grupo de palabras que indica quién realiza la acción del verbo. Pronombre significa la palabra que sustituye a un sustantivo ya conocido para no repetirlo.

Operadores de ausencia: ?., ?? y ??=
Lección 5 de 43 · El lenguaje: de JavaScript a TypeScript estricto

Recorrer datos: map, filter, find, reduce y Object.*
Lección 4 de 43 · El lenguaje: de JavaScript a TypeScript estricto