JavaScript Decorators: What They Are and When to Use Them
On januari 26, 2022 by adminMet de introductie van ES2015+, en nu transpilatie gemeengoed is geworden, zullen velen van jullie nieuwere taalfuncties zijn tegengekomen, hetzij in echte code, hetzij in tutorials. Een van deze functies die mensen vaak op hun hoofd laat krabben als ze ze voor het eerst tegenkomen zijn JavaScript decorators.
Decorators zijn populair geworden dankzij hun gebruik in Angular 2+. In Angular zijn decorators beschikbaar dankzij TypeScript, maar in JavaScript zijn ze momenteel een fase 2-voorstel, wat betekent dat ze deel moeten uitmaken van een toekomstige update van de taal. Laten we eens kijken wat decorators zijn en hoe ze kunnen worden gebruikt om uw code schoner en begrijpelijker te maken.
Wat is een decorator?
In zijn eenvoudigste vorm is een decorator gewoon een manier om een stuk code met een ander stuk code te omhullen – letterlijk “versieren”. Dit is een concept waarvan je misschien al eerder hebt gehoord als functionele compositie, of hogere-orde functies.
Dit is al mogelijk in standaard JavaScript voor veel use-cases, simpelweg door het aanroepen van een functie om een andere te omhullen:
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);
Dit voorbeeld produceert een nieuwe functie – in de variabele wrapped
– die op precies dezelfde manier kan worden aangeroepen als de doSomething
functie, en precies hetzelfde zal doen. Het verschil is dat het wat logging doet voor en na het aanroepen van de gewrapte functie:
doSomething('Graham');// Hello, Grahamwrapped('Graham');// Starting// Hello, Graham// Finished
Hoe JavaScript Decorators te gebruiken
Decorators gebruiken een speciale syntax in JavaScript, waarbij ze voorafgegaan worden door een @
symbool en onmiddellijk geplaatst worden voor de code die gedecoreerd wordt.
Merk op: op het moment van schrijven zijn de decorators in “Stage 2 Draft” vorm, wat betekent dat ze grotendeels af zijn, maar nog onderhevig aan wijzigingen.
Het is mogelijk om zoveel decorators te gebruiken op hetzelfde stuk code als u wenst, en ze zullen worden toegepast in de volgorde waarin u ze declareert.
Voorbeeld:
@log()@immutable()class Example { @time('demo') doSomething() { // }}
Dit definieert een klasse en past drie decorators toe – twee op de klasse zelf, en één op een eigenschap van de klasse:
-
@log
zou alle toegang tot de klasse kunnen loggen -
@immutable
zou de klasse onveranderlijk kunnen maken – misschien roept hijObject.freeze
aan op nieuwe instanties -
@time
zal bijhouden hoe lang het duurt om een methode uit te voeren en dit loggen met een unieke tag.
Op dit moment vereist het gebruik van decorators ondersteuning van de transpiler, omdat geen enkele huidige browser of Node versie er ondersteuning voor heeft. Als je Babel gebruikt, is dit mogelijk door simpelweg de transform-decorators-legacy plugin te gebruiken.
Note: het gebruik van het woord “legacy” in deze plugin is omdat het de Babel 5 manier van omgaan met decorators ondersteunt, die wel eens anders zou kunnen zijn dan de uiteindelijke vorm wanneer ze gestandaardiseerd worden.
Waarom decorators gebruiken?
Terwijl functionele compositie al mogelijk is in JavaScript, is het aanzienlijk moeilijker – of zelfs onmogelijk – om dezelfde technieken toe te passen op andere stukken code (b.v. klassen en klasse-eigenschappen).
Het decorator-voorstel voegt ondersteuning toe voor klasse- en eigenschap-decorators die kunnen worden gebruikt om deze problemen op te lossen, en toekomstige JavaScript-versies zullen waarschijnlijk decorator-ondersteuning toevoegen voor andere problematische gebieden van code.
Decorators zorgen ook voor een schonere syntaxis voor het toepassen van deze wrappers rond uw code, wat resulteert in iets dat minder afbreuk doet aan de eigenlijke bedoeling van wat u schrijft.
Different Types of Decorator
Op dit moment zijn de enige soorten decorator die worden ondersteund, op klassen en leden van klassen. Dit omvat eigenschappen, methoden, getters, en setters.
Decorateurs zijn eigenlijk niets meer dan functies die een andere functie teruggeven, en die worden aangeroepen met de juiste details van het item dat wordt gedecoreerd. Deze decoratorfuncties worden een keer geëvalueerd wanneer het programma voor het eerst wordt uitgevoerd, en de versierde code wordt vervangen door de retourwaarde.
Class member decorators
Property decorators worden toegepast op een enkel lid in een klasse – of dit nu properties, methods, getters, of setters zijn. Deze decorator-functie wordt aangeroepen met drie parameters:
-
target
: de klasse waartoe het lid behoort. -
name
: de naam van het lid in de klasse. -
descriptor
: de lid-descriptor. Dit is in wezen het object dat zou zijn doorgegeven aan Object.defineProperty.
Het klassieke voorbeeld dat hier wordt gebruikt is @readonly
. Dit is zo eenvoudig geïmplementeerd als:
function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}
Het letterlijk bijwerken van de eigenschapsdescriptor om de vlag “beschrijfbaar” op false te zetten.
Dit wordt dan als volgt gebruikt op een klasse-eigenschap:
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>'
Maar we kunnen het beter doen dan dit. We kunnen eigenlijk de versierde functie vervangen door ander gedrag. Laten we bijvoorbeeld alle inputs en outputs loggen:
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;}
Dit vervangt de hele methode door een nieuwe die de argumenten logt, de originele methode aanroept en dan de output logt.
Merk op dat we hier de spread operator hebben gebruikt om automatisch een array te bouwen van alle gegeven argumenten, wat het modernere alternatief is voor de oude arguments
waarde.
We kunnen dit als volgt in gebruik zien:
class Example { @log sum(a, b) { return a + b; }}const e = new Example();e.sum(1, 2);// Arguments: 1,2// Result: 3
Je zult merken dat we een ietwat grappige syntaxis moesten gebruiken om de versierde methode uit te voeren. Dit zou een heel artikel op zichzelf kunnen zijn, maar in het kort, de apply
functie staat u toe om de functie aan te roepen, met het specificeren van de this
waarde en de argumenten om het aan te roepen.
Het opvoeren van een inkeping, kunnen we ervoor zorgen dat onze decorator een aantal argumenten neemt. Laten we bijvoorbeeld onze log
decorator als volgt herschrijven:
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; };}
Dit wordt nu ingewikkelder, maar als we het opsplitsen hebben we dit:
- Een functie,
log
, die een enkele parameter neemt:name
. - Deze functie retourneert vervolgens een functie die zelf een decorator is.
Deze is identiek aan de eerdere log
decorator, behalve dat deze gebruik maakt van de name
parameter uit de buitenste functie.
Dit wordt vervolgens als volgt gebruikt:
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
Meteen kunnen we zien dat dit ons in staat stelt om onderscheid te maken tussen verschillende logregels met behulp van een tag die we zelf hebben aangeleverd.
Dit werkt omdat de log('some tag')
functie-aanroep meteen door de JavaScript runtime wordt geëvalueerd, en vervolgens wordt de respons daarvan gebruikt als de decorator voor de sum
methode.
Klasse-decoratoren
Klasse-decoratoren worden in één keer op de hele klasse-definitie toegepast. De decorator functie wordt aangeroepen met een enkele parameter, dat is de constructor functie die wordt gedecoreerd.
Merk op dat dit wordt toegepast op de constructor functie en niet op elke instantie van de klasse die wordt gemaakt. Dit betekent dat als je de instanties wilt manipuleren, je dat zelf moet doen door een gewrapte versie van de constructor terug te sturen.
In het algemeen zijn deze minder nuttig dan klasse member decorators, omdat alles wat je hier kunt doen, je op precies dezelfde manier kunt doen met een simpele functie-aanroep. Alles wat je met deze decorators doet, moet eindigen met het teruggeven van een nieuwe constructorfunctie die de klasseconstructor vervangt.
Terugkomend op ons log-voorbeeld, laten we er een schrijven die de constructor-parameters logt:
function log(Class) { return (...args) => { console.log(args); return new Class(...args); };}
Hier accepteren we een klasse als ons argument, en geven een nieuwe functie terug die als constructor zal fungeren. Deze logt eenvoudig de argumenten en geeft een nieuwe instantie terug van de klasse die met die argumenten is geconstrueerd.
Voorbeeld:
@logclass Example { constructor(name, age) { }}const e = new Example('Graham', 34);// console.log(e);// Example {}
We kunnen zien dat het construeren van onze Voorbeeld-klasse de opgegeven argumenten logt en dat de geconstrueerde waarde inderdaad een instantie is van Example
. Precies wat we wilden.
Het invoeren van parameters in klasse-decoratoren werkt precies hetzelfde als voor klasse-leden:
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 {}
Real World Examples
Core decorators
Er is een fantastische bibliotheek, Core Decorators genaamd, die een aantal zeer nuttige algemene decorators biedt die nu al klaar zijn voor gebruik. Deze maken over het algemeen zeer nuttige gemeenschappelijke functionaliteit mogelijk (bijv. timing van methode-aanroepen, deprecatiewaarschuwingen, ervoor zorgen dat een waarde alleen-lezen is), maar met gebruikmaking van de veel schonere decoratorsyntaxis.
React
De React-bibliotheek maakt zeer goed gebruik van het concept van Higher-Order Components. Dit zijn simpelweg React-componenten die zijn geschreven als een functie, en die zich om een andere component wikkelen.
Koop onze Premium cursus: React The ES6 Way
Deze zijn een ideale kandidaat om als decorator te gebruiken, omdat je er maar weinig voor hoeft te veranderen. De react-redux bibliotheek heeft bijvoorbeeld een functie, connect
, die wordt gebruikt om een React component aan een Redux store te koppelen.
In het algemeen zou dit als volgt worden gebruikt:
class MyReactComponent extends React.Component {}export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
Omwille van hoe de decorator syntax werkt, kan dit echter worden vervangen door de volgende code om exact dezelfde functionaliteit te bereiken:
@connect(mapStateToProps, mapDispatchToProps)export default class MyReactComponent extends React.Component {}
MobX
De MobX-bibliotheek maakt uitgebreid gebruik van decorators, waarmee u eenvoudig velden kunt markeren als Observable of Computed, en klassen kunt markeren als Observers.
Samenvatting
De decoratoren van klasse leden bieden een zeer goede manier om code binnen een klasse te verpakken op een zeer vergelijkbare manier als hoe u dit reeds kunt doen voor vrijstaande functies. Dit is een goede manier om eenvoudige hulpcode te schrijven die op veel plaatsen kan worden toegepast op een zeer schone en gemakkelijk te begrijpen manier.
De enige limiet voor het gebruik van een dergelijke faciliteit is je verbeelding.
Geef een antwoord