Decoratoare JavaScript: Ce sunt și când să le folosiți
On ianuarie 26, 2022 by adminCu introducerea ES2015+ și pe măsură ce transpilarea a devenit un lucru obișnuit, mulți dintre voi vor fi întâlnit caracteristici mai noi ale limbajului, fie în codul real, fie în tutoriale. Una dintre aceste caracteristici care adesea îi face pe oameni să se scarpine în cap atunci când le întâlnesc pentru prima dată sunt decoratoarele JavaScript.
Decoratoarele au devenit populare datorită utilizării lor în Angular 2+. În Angular, decoratorii sunt disponibili datorită TypeScript, dar în JavaScript sunt în prezent o propunere în stadiul 2, ceea ce înseamnă că ar trebui să facă parte dintr-o viitoare actualizare a limbajului. Să aruncăm o privire asupra a ceea ce sunt decoratorii și asupra modului în care pot fi utilizați pentru a vă face codul mai curat și mai ușor de înțeles.
Ce este un decorator?
În forma sa cea mai simplă, un decorator este pur și simplu o modalitate de a înfășura o bucată de cod cu o alta – literalmente „decorând-o”. Acesta este un concept de care este foarte posibil să fi auzit anterior ca fiind compoziție funcțională sau funcții de ordin superior.
Acest lucru este deja posibil în JavaScript standard pentru multe cazuri de utilizare, pur și simplu prin apelarea unei funcții pentru a înveli o alta:
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);
Acest exemplu produce o nouă funcție – în variabila wrapped
– care poate fi apelată exact în același mod ca și funcția doSomething
și va face exact același lucru. Diferența constă în faptul că va face unele înregistrări înainte și după ce funcția înfășurată este apelată:
doSomething('Graham');// Hello, Grahamwrapped('Graham');// Starting// Hello, Graham// Finished
Cum se utilizează decoratorii JavaScript
Decoratorii folosesc o sintaxă specială în JavaScript, prin care sunt prefixați cu un simbol @
și plasați imediat înainte de codul care este decorat.
Atenție: în momentul scrierii acestui articol, decoratorii se află în prezent în forma „Stage 2 Draft”, ceea ce înseamnă că sunt în mare parte finalizați, dar încă mai pot suferi modificări.
Este posibil să folosiți cât de mulți decoratori pe aceeași bucată de cod doriți, iar aceștia vor fi aplicați în ordinea în care i-ați declarat.
De exemplu:
@log()@immutable()class Example { @time('demo') doSomething() { // }}
Aceasta definește o clasă și aplică trei decoratori – doi la clasa însăși și unul la o proprietate a clasei:
-
@log
ar putea înregistra toate accesările clasei -
@immutable
ar putea face clasa imuabilă – poate cheamăObject.freeze
la noile instanțe -
@time
va înregistra cât timp durează executarea unei metode și va înregistra acest lucru cu o etichetă unică.
În prezent, utilizarea decoratorilor necesită suport pentru transpiler, deoarece niciun browser actual sau versiune Node nu are încă suport pentru acestea. Dacă folosiți Babel, acest lucru este activat prin simpla utilizare a plugin-ului transform-decorators-legacy.
Nota: utilizarea cuvântului „legacy” în acest plugin se datorează faptului că acesta suportă modul Babel 5 de tratare a decoratorilor, care ar putea foarte bine să fie diferit de forma finală atunci când vor fi standardizate.
De ce să folosim decoratori?
În timp ce compoziția funcțională este deja posibilă în JavaScript, este semnificativ mai dificil – sau chiar imposibil – să aplicăm aceleași tehnici la alte părți de cod (de exemplu, clase și proprietăți de clasă).
Propunerea de decorator adaugă suport pentru decoratori de clase și proprietăți care pot fi utilizați pentru a rezolva aceste probleme, iar versiunile viitoare ale JavaScript vor adăuga probabil suport pentru decoratori pentru alte zone de cod problematice.
Decoratoarele permit, de asemenea, o sintaxă mai curată pentru a aplica aceste învelișuri în jurul codului dumneavoastră, rezultând ceva care afectează mai puțin intenția reală a ceea ce scrieți.
Diferite tipuri de decorator
În prezent, singurele tipuri de decorator care sunt acceptate sunt pe clase și membri ai claselor. Acestea includ proprietăți, metode, getters și setters.
Decoratorii nu sunt de fapt altceva decât funcții care returnează o altă funcție și care sunt apelate cu detaliile corespunzătoare ale elementului care este decorat. Aceste funcții decoratoare sunt evaluate o singură dată când programul rulează pentru prima dată, iar codul decorat este înlocuit cu valoarea de retur.
Decoratoare de membri de clasă
Decoratoarele de proprietăți se aplică unui singur membru dintr-o clasă – fie că este vorba de proprietăți, metode, getters sau setters. Această funcție decorator este apelată cu trei parametri:
-
target
: clasa în care se află membrul. -
name
: numele membrului din clasă. -
descriptor
: descriptorul membrului. Acesta este, în esență, obiectul care ar fi fost transmis la Object.defineProperty.
Exemplul clasic utilizat aici este @readonly
. Acest lucru este implementat atât de simplu ca:
function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}
Literalmente actualizarea descriptorului de proprietate pentru a seta steagul „writable” la false.
Acesta este apoi folosit pe o proprietate de clasă după cum urmează:
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>'
Dar putem face mai mult decât atât. Putem de fapt să înlocuim funcția decorată cu un comportament diferit. De exemplu, să înregistrăm toate intrările și ieșirile:
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;}
Aceasta înlocuiește întreaga metodă cu una nouă care înregistrează argumentele, apelează metoda originală și apoi înregistrează ieșirea.
Rețineți că am folosit aici operatorul spread pentru a construi automat un tablou din toate argumentele furnizate, care este alternativa mai modernă la vechea valoare arguments
.
Puteți vedea acest lucru în utilizare după cum urmează:
class Example { @log sum(a, b) { return a + b; }}const e = new Example();e.sum(1, 2);// Arguments: 1,2// Result: 3
Vă veți da seama că a trebuit să folosim o sintaxă ușor ciudată pentru a executa metoda decorată. Acest lucru ar putea acoperi un întreg articol de sine stătător, dar pe scurt, funcția apply
vă permite să apelați funcția, specificând valoarea this
și argumentele cu care să o apelați.
Creând o treaptă superioară, putem aranja ca decoratorul nostru să primească câteva argumente. De exemplu, să rescriem decoratorul nostru log
după cum urmează:
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; };}
Acest lucru devine mai complex acum, dar când îl descompunem avem următorul lucru:
- O funcție,
log
, care ia un singur parametru:name
. - Această funcție returnează apoi o funcție care este ea însăși un decorator.
Aceasta este identică cu decoratorul anterior log
, cu excepția faptului că folosește parametrul name
din funcția exterioară.
Aceasta este apoi folosită după cum urmează:
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
Direct, putem vedea că acest lucru ne permite să distingem între diferite linii de jurnal folosind o etichetă pe care am furnizat-o noi înșine.
Acest lucru funcționează deoarece apelul funcției log('some tag')
este evaluat imediat de către timpul de execuție JavaScript, iar apoi răspunsul de acolo este folosit ca decorator pentru metoda sum
.
Decoratori de clasă
Decoratorii de clasă sunt aplicați întregii definiții a clasei dintr-o singură dată. Funcția decorator este apelată cu un singur parametru care este funcția de constructor care este decorată.
Rețineți că aceasta se aplică funcției de constructor și nu fiecărei instanțe a clasei care este creată. Acest lucru înseamnă că, dacă doriți să manipulați instanțele, trebuie să faceți acest lucru dumneavoastră prin returnarea unei versiuni înfășurate a constructorului.
În general, acestea sunt mai puțin utile decât decoratorii membrilor clasei, deoarece tot ceea ce puteți face aici puteți face cu un simplu apel de funcție în exact același mod. Orice lucru pe care îl faceți cu acestea trebuie să sfârșească prin a returna o nouă funcție de constructor care să înlocuiască constructorul clasei.
Întorcându-ne la exemplul nostru de logare, să scriem unul care înregistrează parametrii constructorului:
function log(Class) { return (...args) => { console.log(args); return new Class(...args); };}
Aici acceptăm o clasă ca argument și returnăm o nouă funcție care va acționa ca și constructor. Aceasta înregistrează pur și simplu argumentele și returnează o nouă instanță a clasei construite cu acele argumente.
De exemplu:
@logclass Example { constructor(name, age) { }}const e = new Example('Graham', 34);// console.log(e);// Example {}
Vezi că prin construirea clasei noastre Exemplu se vor înregistra argumentele furnizate și că valoarea construită este într-adevăr o instanță a clasei Example
. Exact ceea ce am vrut.
Pasarea parametrilor în decoratorii de clasă funcționează exact la fel ca pentru membrii clasei:
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 {}
Exemple din lumea reală
Decoratori de bază
Există o bibliotecă fantastică numită Core Decorators care oferă câțiva decoratori comuni foarte utili care sunt gata de utilizare chiar acum. Aceștia permit, în general, o funcționalitate comună foarte utilă (de exemplu, sincronizarea apelurilor de metode, avertismente de depreciere, asigurarea faptului că o valoare este doar pentru citire), dar utilizând o sintaxă de decorator mult mai curată.
React
Librăria React utilizează foarte bine conceptul de Higher-Order Components. Acestea sunt pur și simplu componente React care sunt scrise ca o funcție și care se înfășoară în jurul unei alte componente.
Cumpărați cursul nostru Premium: React The ES6 Way
Acestea sunt un candidat ideal pentru a fi folosite ca decorator, deoarece sunt foarte puține lucruri pe care trebuie să le modificați pentru a face acest lucru. De exemplu, biblioteca react-redux are o funcție, connect
, care este utilizată pentru a conecta o componentă React la un magazin Redux.
În general, aceasta ar fi folosită după cum urmează:
class MyReactComponent extends React.Component {}export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
Cu toate acestea, datorită modului în care funcționează sintaxa decoratorului, aceasta poate fi înlocuită cu următorul cod pentru a obține exact aceeași funcționalitate:
@connect(mapStateToProps, mapDispatchToProps)export default class MyReactComponent extends React.Component {}
MobX
Librăria MobX utilizează pe scară largă decoratorii, permițându-vă să marcați cu ușurință câmpurile ca Observable sau Computed și să marcați clasele ca Observers.
Summary
Decoratorii membrilor de clasă oferă o modalitate foarte bună de a înfășura codul în interiorul unei clase într-un mod foarte asemănător cu modul în care se poate face deja acest lucru pentru funcțiile independente. Acest lucru oferă o modalitate bună de a scrie un cod de ajutor simplu care poate fi aplicat într-o mulțime de locuri, într-o manieră foarte curată și ușor de înțeles.
Singura limită pentru utilizarea unei astfel de facilități este imaginația dumneavoastră!
Lasă un răspuns