Decorator e inoltro delle chiamate in JavaScript: call e apply
Impara a scrivere funzioni decorator in JavaScript e a inoltrare le chiamate con func.call e func.apply, inclusi caching decorator, bind e method borrowing.
Un decorator è una funzione wrapper: prende un'altra funzione e restituisce una nuova funzione che aggiunge comportamenti — logging, caching, timing, controlli di accesso — attorno all'originale, senza modificarne il codice. Per costruire decorator che funzionino con qualsiasi funzione, occorre un modo affidabile di invocare una funzione con un this scelto e un insieme di argomenti scelto. Questo è esattamente ciò che func.call e func.apply offrono.
Questo capitolo tratta le funzioni decorator (wrapper), l'inoltro di this e degli argomenti con call/apply, il ripristino del contesto perso con bind e il method borrowing.
Nota: Questo riguarda i decorator di funzione — il pattern comune disponibile in JavaScript standard oggi. I più recenti decorator di classe con prefisso
@sono una funzionalità separata e più avanzata (attualmente una proposta Stage 3 che richiede un transpiler) e non vengono trattati qui.
Cos'è un decorator
Un decorator è una funzione che avvolge una funzione target e restituisce un sostituto con comportamento aggiuntivo. Poiché il wrapper ha la stessa interfaccia esterna, i chiamanti non devono cambiare nulla.
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
// 5Il wrapper è riutilizzabile, mantiene l'originale intatto e può essere composto. Il limite dell'esempio sopra è che gestisce solo una funzione con esattamente due argomenti e nessun this. Per avvolgere qualsiasi funzione, occorre inoltrare la chiamata.
Un caching decorator
Un decorator molto comune in pratica mette in cache i risultati affinché una funzione costosa venga eseguita una sola volta per ogni input. Provalo:
Questo funziona per una funzione standalone. Ma nel momento in cui slow è un metodo che usa this, chiamare func(x) lo rompe — il wrapper perde il contesto dell'object. È qui che entrano in gioco call e apply.
Inoltro della chiamata: call e apply
call e apply invocano entrambi una funzione con un this esplicitamente scelto. Differiscono solo nel modo in cui vengono passati gli argomenti:
func.call(thisArg, arg1, arg2, ...)— argomenti elencati singolarmente.func.apply(thisArg, argsArray)— argomenti come un singolo array (o array-like).
call
apply
Queste due chiamate sono equivalenti:
func.call(obj, 1, 2, 3);
func.apply(obj, [1, 2, 3]);Usa call quando conosci gli argomenti singolarmente; usa apply quando li hai già in un array. Con la sintassi spread (func.call(obj, ...args)) la distinzione spesso scompare — vedi Parametri rest e sintassi spread.
Inoltro di this con call
Ora possiamo correggere il caching decorator per i metodi. All'interno del wrapper, this è l'object su cui il metodo è stato chiamato, quindi lo inoltriamo con func.call(this, x):
Senza func.call(this, x), la chiamata interna sarebbe func(x) e this andrebbe perso, quindi this.someMethod() fallirebbe.
Inoltro di tutti gli argomenti con apply
Per un metodo con più argomenti, si inoltrano tutti gli argomenti in una sola volta. Il wrapper non sa quanti ce ne siano, quindi li legge da arguments e li passa tutti tramite func.apply(this, arguments):
Passare this e arguments direttamente è chiamato call forwarding: il wrapper si comporta esattamente come l'originale, con l'aggiunta di logica extra attorno ad esso.
Method borrowing
La funzione hash qui sopra usa un trucco. arguments è array-like (ha indici e length) ma non è un vero array, quindi non ha join. Invece di convertirlo, prendiamo in prestito il metodo dell'array:
function hash(args) {
return [].join.call(args, ',');
}
console.log(hash([3, 5])); // "3,5"[].join è Array.prototype.join. Chiamarlo con args come this esegue la logica di join sul valore array-like. Il method borrowing consente di riutilizzare i metodi built-in su object che non sono di quel tipo.
bind e la perdita del contesto
call e apply invocano immediatamente. bind invece restituisce una nuova funzione con this fissato in modo permanente — utile quando la chiamata avviene in seguito (una callback, un event handler, un setTimeout).
Il problema che bind risolve è la perdita del contesto: staccare un metodo dal suo object fa sì che this non punti più ad esso.
Per un approfondimento su come correggere il contesto nelle callback e sulla differenza tra bind, le arrow function e call/apply, vedi Function binding.
Quando usare quale
| Obiettivo | Usa |
|---|---|
Chiamare subito con this scelto, argomenti elencati singolarmente | func.call(thisArg, a, b) |
Chiamare subito con this scelto, argomenti già in un array | func.apply(thisArg, args) |
Ottenere una funzione da chiamare in seguito con this fisso | func.bind(thisArg) |
| Riutilizzare un metodo built-in su un object array-like | prendilo in prestito: [].method.call(obj, …) |
Conclusione
I decorator avvolgono una funzione per aggiungere comportamento senza modificarla. Per far funzionare un wrapper con qualsiasi funzione — inclusi i metodi — si inoltra la chiamata originale con func.call(this, ...) o func.apply(this, arguments), si usa bind quando la chiamata è differita, e si prendono in prestito i metodi built-in quando un object è solo array-like. Insieme, questi strumenti consentono di creare astrazioni riutilizzabili e context-safe come il caching decorator visto sopra.
Letture correlate: Metodi degli object, "this", Function object, NFE e Function binding.