JavaScript Decorators: Cosa sono e quando usarli
Il Gennaio 26, 2022 da adminCon l’introduzione di ES2015+, e dato che la transpilazione è diventata comune, molti di voi si saranno imbattuti in nuove caratteristiche del linguaggio, sia nel codice reale che nei tutorial. Una di queste caratteristiche che spesso fa grattare la testa alle persone quando le incontrano per la prima volta sono i decoratori JavaScript.
I decoratori sono diventati popolari grazie al loro uso in Angular 2+. In Angular, i decoratori sono disponibili grazie a TypeScript, ma in JavaScript sono attualmente una proposta di fase 2, il che significa che dovrebbero essere parte di un futuro aggiornamento del linguaggio. Diamo un’occhiata a cosa sono i decoratori e a come possono essere usati per rendere il vostro codice più pulito e più facilmente comprensibile.
Che cos’è un decoratore?
Nella sua forma più semplice, un decoratore è semplicemente un modo di avvolgere un pezzo di codice con un altro – letteralmente “decorandolo”. Questo è un concetto che potreste aver già sentito in precedenza come composizione funzionale, o funzioni di ordine superiore.
Questo è già possibile in JavaScript standard per molti casi d’uso, semplicemente chiamando una funzione per avvolgerne un’altra:
function doSomething(name) { console.log('Hello, ' + name);}function loggingDecorator(wrapped) { return function() { console.log('Starting'); const result = wrapped.apply(this, arguments); console.log('Finished'); return result; }}const wrapped = loggingDecorator(doSomething);
Questo esempio produce una nuova funzione – nella variabile wrapped
– che può essere chiamata esattamente allo stesso modo della funzione doSomething
, e farà esattamente la stessa cosa. La differenza è che farà un po’ di log prima e dopo la chiamata della funzione wrapped:
doSomething('Graham');// Hello, Grahamwrapped('Graham');// Starting// Hello, Graham// Finished
Come usare i decoratori JavaScript
I decoratori usano una sintassi speciale in JavaScript, per cui sono preceduti da un simbolo @
e posti immediatamente prima del codice da decorare.
Nota: al momento della scrittura, i decoratori sono attualmente in forma di “Fase 2 Draft”, il che significa che sono per lo più finiti ma ancora soggetti a modifiche.
È possibile utilizzare tutti i decoratori che si desidera sullo stesso pezzo di codice, e saranno applicati nell’ordine in cui li si dichiara.
Per esempio:
@log()@immutable()class Example { @time('demo') doSomething() { // }}
Questo definisce una classe e applica tre decoratori – due alla classe stessa e uno a una proprietà della classe:
-
@log
potrebbe registrare tutti gli accessi alla classe -
@immutable
potrebbe rendere la classe immutabile – forse chiamaObject.freeze
sulle nuove istanze -
@time
registrerà quanto tempo impiega un metodo per essere eseguito e lo registrerà con un tag unico.
Al momento, l’uso dei decoratori richiede il supporto del transpiler, poiché nessun browser o release di Node attuale li supporta ancora. Se stai usando Babel, questo è abilitato semplicemente usando il plugin transform-decorators-legacy.
Nota: l’uso della parola “legacy” in questo plugin è perché supporta il modo in cui Babel 5 gestisce i decoratori, che potrebbe essere diverso dalla forma finale quando saranno standardizzati.
Perché usare i decoratori?
Mentre la composizione funzionale è già possibile in JavaScript, è significativamente più difficile – o addirittura impossibile – applicare le stesse tecniche ad altri pezzi di codice (per esempio classi e proprietà di classe).
La proposta del decoratore aggiunge il supporto per i decoratori di classe e di proprietà che possono essere usati per risolvere questi problemi, e le future versioni di JavaScript probabilmente aggiungeranno il supporto del decoratore per altre aree problematiche del codice.
I decoratori permettono anche una sintassi più pulita per l’applicazione di questi involucri intorno al codice, risultando in qualcosa che distrae meno dall’effettiva intenzione di ciò che si sta scrivendo.
Diversi tipi di decoratore
Al momento, gli unici tipi di decoratore che sono supportati sono sulle classi e sui membri delle classi. Questo include proprietà, metodi, getter e setter.
I decoratori non sono altro che funzioni che restituiscono un’altra funzione e che sono chiamate con i dettagli appropriati dell’elemento decorato. Queste funzioni decoratrici vengono valutate una volta quando il programma viene eseguito per la prima volta, e il codice decorato viene sostituito con il valore di ritorno.
Decoratori di membri di classe
I decoratori di proprietà si applicano a un singolo membro di una classe – che siano proprietà, metodi, getter o setter. Questa funzione decoratore è chiamata con tre parametri:
-
target
: la classe in cui si trova il membro. -
name
: il nome del membro nella classe. -
descriptor
: il descrittore del membro. Questo è essenzialmente l’oggetto che sarebbe stato passato a Object.defineProperty.
L’esempio classico usato qui è @readonly
. Questo è implementato semplicemente come:
function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}
Semplicemente aggiornando il descrittore della proprietà per impostare il flag “writable” su false.
Questo è poi usato su una proprietà di classe come segue:
class Example { a() {} @readonly b() {}}const e = new Example();e.a = 1;e.b = 2;// TypeError: Cannot assign to read only property 'b' of object '#<Example>'
Ma possiamo fare meglio di così. Possiamo effettivamente sostituire la funzione decorata con un comportamento diverso. Per esempio, registriamo tutti gli input e gli output:
function log(target, name, descriptor) { const original = descriptor.value; if (typeof original === 'function') { descriptor.value = function(...args) { console.log(`Arguments: ${args}`); try { const result = original.apply(this, args); console.log(`Result: ${result}`); return result; } catch (e) { console.log(`Error: ${e}`); throw e; } } } return descriptor;}
Questo sostituisce l’intero metodo con uno nuovo che registra gli argomenti, chiama il metodo originale e poi registra l’output.
Nota che abbiamo usato l’operatore spread qui per costruire automaticamente un array da tutti gli argomenti forniti, che è l’alternativa più moderna al vecchio valore arguments
.
Possiamo vederlo in uso come segue:
class Example { @log sum(a, b) { return a + b; }}const e = new Example();e.sum(1, 2);// Arguments: 1,2// Result: 3
Si noterà che abbiamo dovuto usare una sintassi leggermente divertente per eseguire il metodo decorato. Questo potrebbe coprire un intero articolo a sé stante, ma in breve, la funzione apply
permette di chiamare la funzione, specificando il valore this
e gli argomenti con cui chiamarla.
Superando il limite, possiamo fare in modo che il nostro decoratore prenda degli argomenti. Per esempio, riscriviamo il nostro decoratore log
come segue:
function log(name) { return function decorator(t, n, descriptor) { const original = descriptor.value; if (typeof original === 'function') { descriptor.value = function(...args) { console.log(`Arguments for ${name}: ${args}`); try { const result = original.apply(this, args); console.log(`Result from ${name}: ${result}`); return result; } catch (e) { console.log(`Error from ${name}: ${e}`); throw e; } } } return descriptor; };}
Questo sta diventando più complesso ora, ma quando lo scomponiamo abbiamo questo:
- Una funzione,
log
, che prende un solo parametro:name
. - Questa funzione restituisce poi una funzione che è a sua volta un decoratore.
Questo è identico al precedente decoratore log
, tranne che fa uso del parametro name
della funzione esterna.
Questo è poi usato come segue:
class Example { @log('some tag') sum(a, b) { return a + b; }}const e = new Example();e.sum(1, 2);// Arguments for some tag: 1,2// Result from some tag: 3
Presto possiamo vedere che questo ci permette di distinguere tra diverse linee di log usando un tag che noi stessi abbiamo fornito.
Questo funziona perché la chiamata alla funzione log('some tag')
viene valutata subito dal runtime JavaScript, e poi la risposta viene usata come decoratore per il metodo sum
.
Decoratori di classe
I decoratori di classe vengono applicati all’intera definizione della classe in una sola volta. La funzione decoratore viene chiamata con un singolo parametro che è la funzione costruttore che viene decorata.
Nota che questo viene applicato alla funzione costruttore e non ad ogni istanza della classe che viene creata. Questo significa che se volete manipolare le istanze dovete farlo voi stessi restituendo una versione wrapped del costruttore.
In generale, questi sono meno utili dei decoratori dei membri della classe, perché tutto ciò che potete fare qui potete farlo con una semplice chiamata di funzione esattamente allo stesso modo. Qualsiasi cosa facciate con questi deve finire per restituire una nuova funzione costruttore per sostituire il costruttore di classe.
Tornando al nostro esempio di log, scriviamone uno che registri i parametri del costruttore:
function log(Class) { return (...args) => { console.log(args); return new Class(...args); };}
Qui stiamo accettando una classe come argomento, e restituendo una nuova funzione che agirà come costruttore. Questo semplicemente registra gli argomenti e restituisce una nuova istanza della classe costruita con quegli argomenti.
Per esempio:
@logclass Example { constructor(name, age) { }}const e = new Example('Graham', 34);// console.log(e);// Example {}
Possiamo vedere che la costruzione della nostra classe Example registra gli argomenti forniti e che il valore costruito è effettivamente un’istanza di Example
. Esattamente quello che volevamo.
Il passaggio di parametri nei decoratori di classe funziona esattamente come per i membri della classe:
function log(name) { return function decorator(Class) { return (...args) => { console.log(`Arguments for ${name}: args`); return new Class(...args); }; }}@log('Demo')class Example { constructor(name, age) {}}const e = new Example('Graham', 34);// Arguments for Demo: argsconsole.log(e);// Example {}
Esempi del mondo reale
Core decorators
C’è una fantastica libreria chiamata Core Decorators che fornisce alcuni decoratori comuni molto utili che sono già pronti all’uso. Questi generalmente permettono funzionalità comuni molto utili (ad esempio la tempistica delle chiamate ai metodi, gli avvisi di deprecazione, assicurando che un valore sia di sola lettura) ma utilizzando la sintassi molto più pulita dei decoratori.
React
La libreria React fa un ottimo uso del concetto di Higher-Order Components. Questi sono semplicemente componenti React che sono scritti come una funzione, e che si avvolgono intorno ad un altro componente.
Acquista il nostro corso Premium: React The ES6 Way
Questi sono un candidato ideale da usare come decoratore, perché c’è molto poco da cambiare per farlo. Per esempio, la libreria react-redux ha una funzione, connect
, che è usata per collegare un componente React ad un negozio Redux.
In generale, questo dovrebbe essere usato come segue:
class MyReactComponent extends React.Component {}export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
Tuttavia, a causa di come funziona la sintassi dei decoratori, questo può essere sostituito con il seguente codice per ottenere esattamente la stessa funzionalità:
@connect(mapStateToProps, mapDispatchToProps)export default class MyReactComponent extends React.Component {}
MobX
La libreria MobX fa ampio uso di decoratori, permettendo di marcare facilmente i campi come Observable o Computed, e marcando le classi come Observers.
Sommario
I decoratori dei membri della classe forniscono un ottimo modo per avvolgere il codice all’interno di una classe in modo molto simile a come si può già fare per le funzioni indipendenti. Questo fornisce un buon modo per scrivere del semplice codice di aiuto che può essere applicato in molti posti in un modo molto pulito e facile da capire.
L’unico limite all’uso di questa struttura è la vostra immaginazione!
Lascia un commento