JavaScript Decorators: Vad de är och när man ska använda dem
On januari 26, 2022 by adminMed introduktionen av ES2015+, och eftersom transpilering har blivit vanligt förekommande, kommer många av er att ha stött på nyare språkfunktioner, antingen i riktig kod eller i handledningar. En av dessa funktioner som ofta får folk att klia sig i huvudet när de först stöter på dem är JavaScript decorators.
Decorators har blivit populära tack vare deras användning i Angular 2+. I Angular är dekoratorer tillgängliga tack vare TypeScript, men i JavaScript är de för närvarande ett steg 2-förslag, vilket innebär att de bör ingå i en framtida uppdatering av språket. Låt oss ta en titt på vad dekoratorer är och hur de kan användas för att göra din kod renare och mer lättförståelig.
Vad är en dekorator?
I sin enklaste form är en dekorator helt enkelt ett sätt att omsluta en kodbit med en annan – bokstavligen ”dekorera” den. Detta är ett koncept som du kanske har hört talas om tidigare som funktionell sammansättning, eller funktioner av högre ordning.
Detta är redan möjligt i standard-JavaScript för många användningsfall, helt enkelt genom att anropa en funktion för att omsluta en annan:
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);
Detta exempel producerar en ny funktion – i variabeln wrapped
– som kan anropas på exakt samma sätt som doSomething
-funktionen, och som kommer att göra exakt samma sak. Skillnaden är att den kommer att göra en viss loggning före och efter att den inplastade funktionen anropas:
doSomething('Graham');// Hello, Grahamwrapped('Graham');// Starting// Hello, Graham// Finished
Hur man använder JavaScript Decorators
Decorators använder en speciell syntax i JavaScript, där de föregås av en @
-symbol och placeras omedelbart före koden som ska dekoreras.
Notera: I skrivande stund är dekoratorerna för närvarande i ”Stage 2 Draft”-form, vilket innebär att de till största delen är färdiga men fortfarande föremål för ändringar.
Det är möjligt att använda så många dekoratorer på samma kodstycke som du önskar, och de kommer att tillämpas i den ordning som du deklarerar dem.
Till exempel:
@log()@immutable()class Example { @time('demo') doSomething() { // }}
Detta definierar en klass och tillämpar tre dekoratorer – två på själva klassen och en på en egenskap i klassen:
-
@log
kan logga all åtkomst till klassen -
@immutable
kan göra klassen oföränderlig – kanske kallar denObject.freeze
på nya instanser -
@time
kommer att registrera hur lång tid det tar för en metod att utföra och logga ut detta med en unik tagg.
För närvarande kräver användningen av dekoratorer stöd för transpiler, eftersom ingen aktuell webbläsare eller Node-version har stöd för dem ännu. Om du använder Babel aktiveras detta helt enkelt genom att använda insticksprogrammet transform-decorators-legacy.
Notera: Användningen av ordet ”legacy” i detta insticksprogram beror på att det stöder Babel 5:s sätt att hantera dekoratorer, vilket mycket väl kan skilja sig från den slutgiltiga formen när de standardiseras.
Varför använda dekoratorer?
Som funktionell sammansättning redan är möjlig i JavaScript är det betydligt svårare – eller till och med omöjligt – att tillämpa samma tekniker på andra delar av koden (t.ex. klasser och klassegenskaper).
Det dekoratorförslaget lägger till stöd för klass- och egenskapsdekoratorer som kan användas för att lösa dessa problem, och framtida JavaScript-versioner kommer förmodligen att lägga till stöd för dekoratorer för andra besvärliga kodområden.
Dekoratorer möjliggör också en renare syntax för att tillämpa dessa omslag runt din kod, vilket resulterar i något som drar mindre av från den faktiska avsikten med det du skriver.
Differentierade typer av dekoratorer
För närvarande är de enda typerna av dekoratorer som stöds på klasser och medlemmar av klasser. Detta inkluderar egenskaper, metoder, getters och setters.
Dekoratorer är egentligen inget annat än funktioner som returnerar en annan funktion och som anropas med lämpliga detaljer för det objekt som dekoreras. Dessa dekoratorfunktioner utvärderas en gång när programmet körs för första gången, och den dekorerade koden ersätts med returvärdet.
Dekoratorer för klassmedlemmar
Dekoratorer för egenskaper tillämpas på en enskild medlem i en klass – oavsett om de är egenskaper, metoder, getters eller setters. Den här dekoratorfunktionen anropas med tre parametrar:
-
target
: klassen som medlemmen ingår i. -
name
: namnet på medlemmen i klassen. -
descriptor
: medlemsbeskrivningen. Detta är i huvudsak det objekt som skulle ha överlämnats till Object.defineProperty.
Det klassiska exemplet som används här är @readonly
. Detta implementeras så enkelt som:
function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}
Bokstavligen uppdaterar man egenskapsbeskrivaren för att sätta flaggan ”writable” till false.
Detta används sedan på en klassegenskap på följande sätt:
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>'
Men vi kan göra bättre än så. Vi kan faktiskt ersätta den dekorerade funktionen med ett annat beteende. Låt oss till exempel logga alla in- och utdata:
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;}
Detta ersätter hela metoden med en ny som loggar argumenten, anropar den ursprungliga metoden och loggar sedan utdata.
Bemärk att vi har använt spread-operatorn här för att automatiskt bygga upp en array från alla angivna argument, vilket är det modernare alternativet till det gamla arguments
-värdet.
Vi kan se detta i användning på följande sätt:
class Example { @log sum(a, b) { return a + b; }}const e = new Example();e.sum(1, 2);// Arguments: 1,2// Result: 3
Du kommer att märka att vi var tvungna att använda en lite lustig syntax för att exekvera den dekorerade metoden. Detta skulle kunna täcka en hel egen artikel, men i korthet kan man säga att apply
-funktionen gör det möjligt att anropa funktionen genom att ange this
-värdet och de argument som den ska anropas med.
Om vi tar det hela ett steg längre upp kan vi ordna så att vår dekorator tar emot några argument. Låt oss till exempel skriva om vår log
decorator på följande sätt:
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; };}
Detta börjar bli mer komplext nu, men när vi bryter ner det får vi följande:
- En funktion,
log
, som tar en enda parameter:name
. - Denna funktion returnerar sedan en funktion som i sin tur är en dekorator.
Denna funktion är identisk med den tidigare dekoratorn log
, förutom att den använder sig av parametern name
från den yttre funktionen.
Denna funktion används sedan på följande sätt:
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
Vi kan direkt se att det här gör det möjligt för oss att skilja mellan olika logglinjer med hjälp av en tagg som vi har tillhandahållit själva.
Detta fungerar eftersom log('some tag')
-funktionsanropet utvärderas av JavaScript-körtiden direkt, och sedan används svaret från det som dekorator för sum
-metoden.
Klassdekoratorer
Klassdekoratorer appliceras på hela klassdefinitionen i ett svep. Dekoratorfunktionen anropas med en enda parameter som är konstruktorfunktionen som ska dekoreras.
Bemärk att detta tillämpas på konstruktorfunktionen och inte på varje instans av klassen som skapas. Detta innebär att om du vill manipulera instanserna måste du göra det själv genom att returnera en inplastad version av konstruktören.
I allmänhet är dessa mindre användbara än dekoratorer för klassmedlemmar, eftersom allt du kan göra här kan du göra med ett enkelt funktionsanrop på exakt samma sätt. Allt du gör med dessa måste sluta med att du returnerar en ny konstruktörfunktion som ersätter klassens konstruktör.
Vi återgår till vårt loggningsexempel, låt oss skriva ett som loggar konstruktörsparametrarna:
function log(Class) { return (...args) => { console.log(args); return new Class(...args); };}
Här tar vi emot en klass som argument och returnerar en ny funktion som kommer att fungera som konstruktör. Denna loggar helt enkelt argumenten och returnerar en ny instans av klassen som konstruerats med dessa argument.
Till exempel:
@logclass Example { constructor(name, age) { }}const e = new Example('Graham', 34);// console.log(e);// Example {}
Vi kan se att konstruktionen av vår Example-klass loggar ut de angivna argumenten och att det konstruerade värdet verkligen är en instans av Example
. Exakt vad vi ville.
Att skicka parametrar till klassdekoratorer fungerar exakt på samma sätt som för klassmedlemmar:
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 {}
Exempel från den verkliga världen
Kärndekoratorer
Det finns ett fantastiskt bibliotek som heter Core Decorators och som tillhandahåller några mycket användbara gemensamma dekoratorer som är redo att användas direkt. Dessa möjliggör i allmänhet mycket användbar gemensam funktionalitet (t.ex. timing av metodanrop, deprecieringsvarningar, säkerställa att ett värde är skrivskyddat) men använder den mycket renare dekorator-syntaxen.
React
Biblioteket React använder sig mycket väl av konceptet Higher-Order Components. Dessa är helt enkelt React-komponenter som är skrivna som en funktion och som sveper runt en annan komponent.
Köp vår Premiumkurs: React The ES6 Way
Dessa är en idealisk kandidat för att användas som dekorator, eftersom det är väldigt lite du behöver ändra för att göra det. Till exempel har react-redux-biblioteket en funktion, connect
, som används för att ansluta en React-komponent till ett Redux-lager.
I allmänhet skulle detta användas på följande sätt:
class MyReactComponent extends React.Component {}export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
På grund av hur dekoratorsyntaxen fungerar kan detta dock ersättas med följande kod för att uppnå exakt samma funktionalitet:
@connect(mapStateToProps, mapDispatchToProps)export default class MyReactComponent extends React.Component {}
MobX
MobX-biblioteket använder sig i stor utsträckning av dekoratorer, vilket gör det möjligt för dig att enkelt markera fält som Observable eller Computed, och markera klasser som Observers.
Sammanfattning
Class member decorators ger ett mycket bra sätt att paketera kod inuti en klass på ett mycket likartat sätt som du redan kan göra det för fristående funktioner. Detta ger ett bra sätt att skriva enkel hjälpkod som kan tillämpas på många ställen på ett mycket rent och lättförståeligt sätt.
Den enda gränsen för att använda en sådan funktion är din fantasi!
Lämna ett svar