Decoradores y reenvío en JavaScript: call y apply
Aprende a escribir funciones decoradoras en JavaScript y reenviar llamadas con func.call y func.apply, incluyendo un decorador de caché, bind y préstamo de métodos.
Un decorador es una función envolvente: toma otra función y devuelve una nueva función que añade comportamiento — registro, caché, temporización, verificaciones de acceso — alrededor de la original, sin tocar su código. Para construir decoradores que funcionen con cualquier función, necesitas una forma fiable de llamar a una función con un this elegido y un conjunto de argumentos elegido. Eso es exactamente lo que proporcionan func.call y func.apply.
Este capítulo cubre las funciones decoradoras (envolventes), el reenvío de this y argumentos con call/apply, la restauración del contexto perdido con bind y el préstamo de métodos.
Nota: Esto trata sobre los decoradores de funciones — el patrón cotidiano disponible en JavaScript puro hoy en día. Los decoradores de clases con prefijo
@son una característica separada y más avanzada (actualmente una propuesta en Stage 3 que requiere un transpilador), y no se cubren aquí.
Qué es un decorador
Un decorador es una función que envuelve una función objetivo y devuelve un reemplazo con comportamiento adicional. Como el envoltorio tiene la misma forma exterior, los llamadores no tienen que cambiar.
function sum(a, b) {
return a + b;
}
function logged(func) {
return function (a, b) {
console.log(`calling with ${a}, ${b}`);
return func(a, b);
};
}
const loggedSum = logged(sum);
console.log(loggedSum(2, 3));
// calling with 2, 3
// 5El envoltorio es reutilizable, mantiene el original intacto y puede apilarse. El problema del ejemplo anterior es que solo maneja una función que toma exactamente dos argumentos y ningún this. Para envolver cualquier función, reenviamos la llamada.
Un decorador de caché
Un decorador habitual en el mundo real almacena en caché los resultados para que una función costosa se ejecute solo una vez por entrada. Pruébalo:
Esto funciona para una función independiente. Pero en el momento en que slow es un método que usa this, llamar a func(x) lo rompe — el envoltorio pierde el contexto del objeto. Ahí es donde entran call y apply.
Reenvío de la llamada: call y apply
call y apply invocan una función con un this elegido explícitamente. Solo difieren en cómo se pasan los argumentos:
func.call(thisArg, arg1, arg2, ...)— argumentos enumerados individualmente.func.apply(thisArg, argsArray)— argumentos como un único array (o similar a un array).
call
apply
Estas dos llamadas son equivalentes:
func.call(obj, 1, 2, 3);
func.apply(obj, [1, 2, 3]);Usa call cuando conoces los argumentos individualmente; usa apply cuando ya los tienes en un array. Con la sintaxis spread (func.call(obj, ...args)) la distinción a menudo desaparece — consulta Parámetros rest y sintaxis spread.
Reenvío de this con call
Ahora podemos arreglar el decorador de caché para métodos. Dentro del envoltorio, this es el objeto sobre el que se llamó el método, así que lo reenviamos con func.call(this, x):
Sin func.call(this, x), la llamada interna sería func(x) y se perdería this, por lo que this.someMethod() fallaría.
Reenvío de todos los argumentos con apply
Para un método con varios argumentos, reenvía todos los argumentos de una vez. El envoltorio no sabe cuántos hay, así que los lee de arguments y los pasa todos con func.apply(this, arguments):
Pasar this y arguments directamente se denomina reenvío de llamada: el envoltorio se comporta exactamente como el original, solo con lógica adicional a su alrededor.
Préstamo de métodos
La función hash anterior usa un truco. arguments es similar a un array (tiene índices y length) pero no es un array real, por lo que no tiene join. En lugar de convertirlo, tomamos prestado el método del array:
function hash(args) {
return [].join.call(args, ',');
}
console.log(hash([3, 5])); // "3,5"[].join es Array.prototype.join. Llamarlo con args como this ejecuta la lógica de join sobre el valor similar a un array. El préstamo de métodos permite reutilizar métodos integrados en objetos que no son de ese tipo.
bind y pérdida de contexto
call y apply invocan de inmediato. bind en cambio devuelve una nueva función con this fijado permanentemente — útil cuando la llamada ocurre más tarde (un callback, un manejador de eventos, un setTimeout).
El problema que bind resuelve es la pérdida de contexto: desvincula un método de su objeto y this deja de apuntar a él.
Para una mirada más detallada sobre cómo fijar el contexto en callbacks y la diferencia entre bind, las funciones flecha y call/apply, consulta Enlace de funciones.
Cuándo usar cada uno
| Objetivo | Usar |
|---|---|
Llamar ahora con this elegido, argumentos enumerados individualmente | func.call(thisArg, a, b) |
Llamar ahora con this elegido, argumentos ya en un array | func.apply(thisArg, args) |
Obtener una función para llamar después con this fijo | func.bind(thisArg) |
| Reutilizar un método integrado en un objeto similar a un array | préstamo: [].method.call(obj, …) |
Conclusión
Los decoradores envuelven una función para añadir comportamiento sin modificarla. Para que un envoltorio funcione con cualquier función — incluyendo métodos — reenvía la llamada original con func.call(this, ...) o func.apply(this, arguments), usa bind cuando la llamada se difiere, y toma prestados métodos integrados cuando un objeto es solo similar a un array. Juntos, estos ofrecen abstracciones reutilizables y seguras con el contexto, como el decorador de caché anterior.
Lectura relacionada: Métodos de objeto, "this", Objeto función, NFE y Enlace de funciones.