Dans cet article, nous aborderons les décorateurs en JavaScript avec leurs cas d'utilisation. Nous découvrirons également tous les concepts liés aux décorateurs avec des exemples pratiques.
Qu'est-ce qu'un décorateur?
Les décorateurs ne sont qu'une enveloppe autour d'une fonction . Ils sont utilisés pour améliorer la fonctionnalité de la fonction sans modifier la fonction sous-jacente . Les décorateurs ne sont pas un nouveau concept – ils ont déjà été utilisés par les développeurs de Python et C #, et même les développeurs JavaScript les ont implémentés depuis toujours. Quelle? Oui, vous l'avez bien entendu.
Les développeurs JavaScript connaissent les fonctions d'ordre supérieur, qui se comportent exactement comme les décorateurs. Pour comprendre les fonctions d'ordre supérieur en JavaScript, nous allons avoir un aperçu détaillé des fonctions en JavaScript.
Fonctions en JavaScript
Les fonctions en JavaScript sont des objets de première classe, c'est-à-dire qu'elles se comportent exactement comme des objets . Nous pouvons les affecter à des variables, les transmettre en tant que paramètres à d'autres fonctions. Ils peuvent également être renvoyés par une autre fonction. Prenons quelques exemples pour comprendre tous ces concepts.
Fonctions assignées à une variable
var helloWorldFunc = fonction () {
console . log ( "Hello, world" )
}
var anotherVar = helloWorldFunc
helloWorldFunc ()
anotherVar ()
Ici, helloWorldFunc
pointe vers une définition de fonction qui imprime Bonjour, monde . Lorsque helloWorldFunc
est affecté à anotherVar
les deux variables commencent à pointer vers la même définition de fonction. Par conséquent, lorsque les deux fonctions sont appelées, elles impriment la même sortie – Hello, world .
Fonctions passées en paramètre à une autre fonction
var printHello = [19659011] fonction () {
console . log ( "Je suis la fonction printHello" )
}
fonction printHelloAndHi ( func ) {
func ()
console . log ( "Je suis la fonction printHelloAndHi" )
}
printHelloAndHi ( printHello )
Ici , nous avons passé la fonction printHello
comme paramètre à la fonction printHelloAndHi
donc la fonction printHelllo
est affectée à la variable func
. printHelloAndHi
la fonction appelle func
(c.-à-d. printHello
) dans sa définition.
Fonction renvoyée par une autre fonction
fonction printAdditionFunc ( x y ) {
var addNumbers = fonction () {
résultat = x + y ;
console . log ( "Ajout de" + x + "et" + y + "est:" + résultat ) ;
}
return addNumbers
}
var addNumbersFunc = printAdditionFunc ( 3 4 )
console . log ( addNumbersFunc )
addNumbersFunc ()
Ici, [19459091] Ici, ] La fonction printAdditionFunc
prend deux paramètres x
et y
. Il renvoie définition de la fonction addNumbers
qui est affectée à addNumbersFunc
. Lorsque addNumbersFunc
est appelé, il renvoie l'ajout de 3
et 4
. Comment 3
et 4
qui ont été passés à printAdditionFunc
accessibles à l'intérieur de addNumbersFunc
? Cela est dû à fermetures dans JavaScript . Les fermetures permettent aux fonctions enfant d'accéder aux variables de fonction parent, aux objets, etc. En d'autres termes, les fonctions enfant stockent les variables de fonction parent afin que les fonctions enfant puissent les utiliser à tout moment.
Dans la fonction ci-dessus, nous pouvons vérifier que addNumbersFunc
stocke les valeurs de 3
et 4
. La façon dont JavaScript stocke ces variables peut être comprise à partir de cet article .
Maintenant que les bases des fonctions sont claires, comprenons les fonctions d'ordre supérieur.
Fonctions d'ordre supérieur en JavaScript
Les fonctions d'ordre sont des fonctions en JavaScript qui prennent une autre fonction en paramètre, ajoutent quelques opérations par-dessus et renvoient la fonction. Les fonctions printAdditionFunc
et printAddition
mentionnées dans les exemples ci-dessus sont toutes deux des fonctions d'ordre supérieur. Prenons quelques exemples de fonctions d'ordre supérieur.
fonction printMessage ( message ) {
return fonction () {
console . log ( message )
}
}
var printHello = printMessage ([[19659017] "Bonjour" )
printHello ()
var printHi = printMessage ( "Salut" )
printHi ()
Ici, printMessage
est une fonction d'ordre supérieur. C'est comme une fonction d'usine qui renvoie une nouvelle fonction.
Un autre exemple plus utile
fonction handleException ( funcAsParameter ) {
console . log ( "Inside handleException function" )
try {
funcAsParameter ()
} [19659011] capture ( err ) {
console . log ( err )
}
}
fonction divideByZero () {
result = 5 / 0
if (! Numéro . isFinite ( result ) ) {
lancer "La division par zéro n'est pas une bonne idée !!"
}
console . log ( "Le résultat de la division de 5 par zéro est:" + résultat )
}
handleException ( divideByZero )
Ici, la fonction handleException
est une fonction d'ordre supérieur. Il peut être utilisé par n'importe quelle fonction pour gérer l'exception. Avec la fonction handleException
nous n'avons pas à gérer les exceptions pour chaque fonction séparément, nous pouvons simplement passer chaque fonction à la fonction handleException
et ils obtiendront la fonctionnalité pour gérer les exceptions de la handleException
. Lorsque nous transmettons divideByZero
en tant que paramètre à la fonction handleException
l'exception déclenchée par divideByZero
est gérée par le code de fonction handleException
. [19659177] Si vous examinez attentivement la définition des fonctions et des décorateurs d'ordre supérieur, vous constaterez qu'ils sont similaires. Les fonctions d'ordre supérieur se comportent exactement comme les décorateurs. Tout comme les fonctions d'ordre supérieur, les décorateurs ajoutent également des fonctionnalités à la fonction existante sans modifier le code sous-jacent. Dans notre cas, handleException
est une fonction de décorateur.
Alors, pourquoi les décorateurs?
Si nous avons déjà un décorateur comme fonction d'ordre supérieur, alors pourquoi avons-nous besoin de décorateurs? Les fonctions d'ordre supérieur peuvent servir de décorateurs à la fonction JavaScript, mais l'invention des classes en JavaScript nous a présenté les méthodes de classe – c'est-à-dire les fonctions définies à l'intérieur de la classe
où les fonctions d'ordre supérieur n'ont pas agi comme décorateurs . Pour comprendre pourquoi une méthode de classe ne peut pas être utilisée avec des fonctions d'ordre supérieur, commençons par comprendre: ce que sont les classes en JavaScript.
Classes en JavaScript
Avant le mot-clé de classe
Avant le Le mot-clé class
a été introduit par ES2015, si nous voulions créer des classes, c'est-à-dire un plan pour les objets, en JavaScript, nous avons pris l'aide de prototypes et de la fonction constructeur
.
Considérons un exemple de la constructeur
fonction:
fonction Humain ( firstName lastName ) {
this . . ] firstName = firstName
this . lastName = lastName
}
Humain . prototype . getFullName = function () {
return this . . firstName + "" + this . lastName
}
var person1 = new Human ( "Virat" "Kohli" ) [19659212] var person2 = new Human ( "Rohit" "Sharma" )
console . log ( person1 )
La fonction Human
est une fonction constructeur avec des propriétés firstName
et nom
. Avec l'aide de cette fonction humaine
nous pouvons créer de nouveaux objets. Ces objets auront comme propriétés firstName
lastName
et getFullName
.
Chaque fonction constructeur a une propriété prototype
qui est un objet. Les propriétés ajoutées au prototype
seront partagées entre tous les objets créés à partir de la fonction constructeur. Les propriétés du prototype peuvent être vérifiées comme suit:
Humain . prototype
getFullName
est définie sur la propriété prototype de la fonction Humain
. Chaque objet créé à l'aide de la fonction constructeur Humain
aura sa propre copie de firstName
et lastName
c'est-à-dire person1
aura firstName
valeur comme Virat tandis que personne2
aura firstName
valeur comme Rohit .
Le getFullName
La fonction sera partagée entre tous les objets créés à l'aide de la fonction constructeur, c'est-à-dire que person1
et person2
auront la même copie de getFullname
. Si person1
change getFullName
person2
aura également cette modification getFullName
. En effet, les propriétés de prototype de la fonction constructeur sont partagées entre tous les objets.
Nous aurions également pu définir getFullName
à l'intérieur de Human
fonction constructeur et il aurait été parfaitement bien. Mais alors, person1
et person2
auraient des copies différentes de la fonction getFullname
qui seraient redondantes. Inutilement, cela aurait consommé de la mémoire supplémentaire. Par conséquent, la fonction getFullName
est définie sur le prototype humain
afin que tous les objets créés à l'aide de la fonction constructeur humain
aient la même copie de getFullName
Ce ne sont que les bases des prototypes pour les décorateurs. Si vous voulez comprendre ce concept important et merveilleux de prototypes, vous pouvez lire cet article
Mot-clé de classe
Les classes en JavaScript, introduites dans ES2015, ne sont pas comme les classes en Java, C # ou Python. C'est juste du sucre syntaxique sur le comportement basé sur un prototype. Le sucre syntaxique signifie que JavaScript vous permet de définir des classes à l'aide du mot-clé class
mais, sous le capot, il utilise toujours des prototypes et des fonctions constructeurs comme indiqué ci-dessus pour créer des objets.
Déclaration de classe
class Humain {
constructeur ( firstName lastName ) {
this . firstName = prénom
this . lastName = lastName
}
getFullName () {
return this . firstName + "" + [19659011] ceci . lastName
}
}
typeof ( Human )
Le code ci-dessus déclare une classe Human
avec une fonction constructor
function constructeur . Sous le capot, JavaScript convertira cette classe en fonction constructeur Humain
. Toutes les fonctions définies à l'intérieur de la classe seront attachées à la propriété prototype de la fonction constructeur.
Le code ci-dessous affiche la conversion des classes en constructeurs et prototypes:
fonction Humain ( firstName lastName ) {
this . firstName = firstName
this . lastName = lastName
}
Humain . prototype . getFullName = function () {
return this . . firstName + "" + this . lastName
}
Ceci est similaire à la fonction constructeur que nous avons discutée ci-dessus. C'est pourquoi les classes sont appelées sucres syntaxiques en JavaScript.
Dans l'image ci-dessus, nous pouvons voir que le type de Humain
est une fonction. De plus, si nous comparons le Human.prototype
des deux classe Human
et constructeur fonction Human
ils sont les mêmes sauf que la propriété constructeur de la classe Human
a une propriété constructor
avec une valeur class Human
tandis que dans le cas de la fonction constructeur, il s'agit de ƒ Human (firstName, lastName)
comme indiqué ci-dessous :
Nous pouvons créer des objets en utilisant la classe avec nouveau
mot-clé comme indiqué ci-dessous.
humanObj = nouveau Humain ( "Virat" "Kohli" )
console . log ( humanObj )
Voyons maintenant pourquoi les fonctions d'ordre supérieur ne fonctionnent pas avec les méthodes de classe.
Problèmes d'utilisation de fonctions d'ordre supérieur avec la classe Méthodes
fonction log ( functionAsParameter ) {
return fonction () {
console . log ( "Execution of" + functionAsParameter . name + "begin" )
functionAsParameter ()
console . log ( "Execution of" + functionAsParameter . nom + "fin" )
}
}
classe Humain {
constructeur ( firstName lastName ) {[19659165] ceci . firstName = firstName
this . lastName = lastName
}
getFullName () {
return this . firstName + "" + [19659011] ceci . lastName
}
}
var humanObj = nouveau Humain ( "Virat" "Kohli" [19659012])
var newGetFullNameFunc = log ( humanObj . getFullName )
newGetFullNameFunc ([19659012)
Le code ci-dessus déclare une fonction log
comme fonction d'ordre supérieur qui prend une autre fonction comme paramètre. La fonction log
est utilisée pour la journalisation. Nous avons créé un objet humanObj
. Essayons d'utiliser la fonction log
avec la fonction getFullName
. La fonction newGetFullNameFunc
est une fonction modifiée getFullName
capable de se connecter. Définition de newGetFullNameFunc
:
var newGetFullNameFunc = fonction () {
console . log ( "Execution of" + functionAsParameter . name + "begin" )
functionAsParameter ()
console . log ( "Execution of" + functionAsParameter . nom + "fin" )
}
Lorsque newGetFullNameFunc
est appelé, il appelle en interne functionAsParameter
c'est-à-dire la fonction getFullName
. Lorsque la fonction getFullName
est appelée depuis newGetFullNameFunc
la valeur de this
à l'intérieur de getFullName
est indéfinie
. D'où le code casse. Pour corriger cela, nous pouvons modifier notre fonction log
comme indiqué ci-dessous:
fonction log ( classObj functionAsParameter ) [19659014] {
return fonction () {
console . log ( "Execution of" + functionAsParameter . name + "begin" )
functionAsParameter . appeler ( classObj )
console . log ( "Execution of" + functionAsParameter . nom + "fin" )
}
}
classe Humain2 {
constructeur ( firstName lastName ) {[19659165] ceci . firstName = firstName
this . lastName = lastName
}
getFullName () {
return this . firstName + "" + [19659011] ceci . lastName
}
}
var humanObj = nouveau Human2 ( "Virat" "Kohli" [19659012])
var newGetFullNameFunc = log ( humanObj humanObj . getFullName )
newunc ()
Dans le code ci-dessus, nous passons également l'objet humanObj
en tant que paramètre à la fonction log
. Ceci est fait pour préserver la valeur de this
. Dans newGetFullNameFunc
nous appelons functionAsParameter
(c'est-à-dire getFullName
) à l'aide de la fonction call
. La fonction call
appellera getFullName
sur l'objet humanObj
c'est-à-dire maintenant la valeur de cet
à l'intérieur de getFullName
sera humanObj
qui a firstName
et lastName
comme propriétés. Par conséquent, ce code fonctionne parfaitement.
Pour rendre la syntaxe du décorateur plus familière aux développeurs, une nouvelle syntaxe décoratrice
a été proposée qui est similaire à la syntaxe du décorateur dans d'autres langues. Nous allons examiner cette syntaxe, mais d'abord, discutons des descripteurs de propriété qui nous aideront à comprendre les décorateurs.
Descripteurs de propriété
Chaque propriété d'objet en JavaScript a des descripteurs de propriété qui sont utilisés pour décrire les attributs ou les métadonnées de la propriété. Les descripteurs de propriété sont eux-mêmes un objet. Voici les descripteurs de propriété associés à chaque propriété d'un objet:
valeur
: valeur actuelle de la propriété de l'objet.inscriptible
:true
oufalse
. La valeur par défaut esttrue
. Indique si la propriété est accessible en écriture ou non.énumérable
:vrai
oufaux
. La valeur par défaut esttrue
. Indique si la propriété est énumérable ou non, c'est-à-dire si elle apparaîtra dans l'itération deobject.keys
.configurable
:true
oufalse
]. La valeur par défaut esttrue
. Indique si les descripteurs de propriété de la propriété peuvent être modifiés ou non.
Considérez un objet comme indiqué ci-dessous:
var humanObj = {
'firstName' : 'Virat'
'lastName' : 'Kohli'
'getFullName' : fonction ( ]) {
retourner ce . firstName + "" + this . lastName ;
}
}
Ici, nous avons un objet humanObj
. Chaque propriété ( firstName
lastName
et getFullName
) aura ses propres descripteurs de propriété. Nous pouvons vérifier la valeur du descripteur de propriété à l'aide de getOwnPropertyDescriptor
comme illustré ci-dessous:
Object . getOwnPropertyDescriptor ( humanObj [19659071] 'firstName' )
getOwnPropertyDescriptor
répertorie tous les descripteurs de propriété de firstName
. De même, lastName
et getFullName
auront leurs propres descripteurs de propriété.
object.defineProperty
object.defineProperty
peut être utilisé pour définir de nouvelles propriétés ou mettre à jour une propriété existante d'un objet. object.defineProperty
prend les paramètres suivants:
objet
: objet sur lequel une nouvelle propriété doit être créée ou une propriété existante doit être mise à jour.nom
: nom du propriété.descripteur
: objet descripteur de propriété.
Définissons une nouvelle propriété avec âge
sur humanObj
.
objet . . defineProperty ( humanObj 'âge' { valeur : 10 } ) ;
Object . getOwnPropertyDescriptor ( humanObj 'age' )
Pour la propriété age
comme nous l'avons fourni uniquement un descripteur de propriété value
comme paramètre de defineProperty
les autres descripteurs de propriété auront les valeurs par défaut. Si la propriété existe déjà sur l'objet, Object.defineProperty
écrasera la propriété avec les nouveaux descripteurs de propriété comme indiqué ci-dessous.
Object . defineProperty ( humanObj 'firstName' { valeur : "Rohit" } ) ;
console . log ( humanObj )
Pour empêcher l'utilisateur de modifier la valeur
d'une propriété d'objet, nous pouvons la rendre en lecture seule en changeant inscriptible
en faux
.
Objet . defineProperty ( humanObj 'firstName' [19659012] { inscriptible : false } ) ;
Maintenant, si nous essayons de changer la propriété, cela ne changera pas. [19659224] humanObj . firstName = 'Virat'
console . log ( humanObj )
Si nous ne voulons pas que les utilisateurs modifient les descripteurs de propriété, nous pouvons les empêcher en définissant la propriété configurable
descripteur de false
.
Maintenant, avec la compréhension de tous les concepts ci-dessus, comprenons enfin les décorateurs.
Remarque : Comme les décorateurs ne font pas actuellement partie de JavaScript, les extraits de code ci-dessous ne peuvent pas être exécutés dans le navigateur. Vous pouvez utiliser JSFiddle pour exécuter et comprendre les extraits de code ci-dessous. Dans JSFiddle, vous devez sélectionner la langue comme Babel + JSX comme indiqué ci-dessous:
Décorateur de méthode de classe
Les décorateurs de méthode de classe sont utilisés pour modifier les méthodes de classe en ajoutant des fonctionnalités supplémentaires au-dessus de la méthode. Il se comporte comme des fonctions d'ordre supérieur. . Les décorateurs sont ajoutés au-dessus de la définition de méthode en utilisant @
suivi du nom de la fonction décorateur. Voici la syntaxe du décorateur:
fonction decoratorFunc ( cible propriété descripteur ) {}
classe DecoEx {
@decoratorFunc
getFullName () {}
}
Une fonction décoratrice accepte trois paramètres comme indiqué ci-dessus:
cible
: Classe de la méthode surpropriété
: nom de la méthode sur laquelle le décorateur est défini.descriptor
: descripteurs de propriété de la méthode sur laquelle le décorateur a été défini.
le décorateur renvoie l'objet descripteur de propriété. Nous pouvons utiliser la propriété de la valeur du descripteur de propriété
pour remplacer la définition de la fonction sous-jacente. Lorsque le moteur JavaScript rencontre le décorateur, il appelle la fonction décorateur en passant la fonction sous-jacente comme paramètre. À l'intérieur de la fonction décorateur, nous définissons une nouvelle fonction au-dessus de la fonction sous-jacente ainsi que de nouvelles lignes de code.
Voyons cela avec un exemple. Considérez le code ci-dessous:
fonction readonlyDecorator ( cible propriété descripteur ) {
console . journal ( "Target:" )
console . journal ( cible )
console . journal ( " nNom du bien" )
console . journal ( propriété )
console . log ( " nDescriptor property" )
console . journal ( descripteur )
descripteur . accessible en écriture = faux
retour descripteur
}
classe Humain {
constructeur ( firstName lastName ) {
this ]. firstName = firstName
this . lastName = lastName
}
@readonlyDecorator
getFullName () {
retourner ceci . firstName + "" + ceci [19659012]. nom
}
}
humanObj = nouveau Humain ( "Virat" "Kohli" )
console . log ( " ngetFullName property value" )
console . log ( humanObj . getFullName )
humanObj . getFullName = "Bonjour"
console . log ( " nAprès avoir modifié la valeur getFullName" )
console . log ( humanObj . getFullName )
Trouvez le code JSFiddle ici .
Nous voulons que la méthode getFullName
soit en lecture seule. Hence, we have defined the @readonlyDecorator
decorator on top of the getFullName
method. @readonlyDecorator
takes Human.prototype
getFullName
method name i.e. getFullName and property descriptor of getFullName
as parameters.
Here, the JavaScript engine will first call the readonlyDecorator
and then getFullName
function. When readonlyDecorator
is called it modifies the getFullName
function using the property descriptor’s value
property, so when JavaScript calls the getFullName
function, it is calling the modified getFullName
function. Let’s check the steps taken by the JavaScript engine to execute the decorators.
funcDescriptor = Object.getOwnPropertyDescriptor(humanObj, 'getFullName')
descriptor = readonlyDecorator(Human.prototype, 'getFullName', funcDescriptor)
Object.defineProperty(Human.prototype, 'getFullName', descriptor);
You can comment the @readonlyDecorator
code from the top of getFullName
method and then try to modify the getFullName
method.
Now, we will see how to define decorators with parameters. Consider the example below:
function log(message) {
function actualLogDecorator(target, property, descriptor) {
console.log("ngetFullName descriptor's value property")
console.log(descriptor.value)
var actualFunction = descriptor.value;
var decoratorFunc = function() {
console.log(message)
return actualFunction.call(this)
}
descriptor.value = decoratorFunc
console.log("nNew descriptor's value property due to decorator")
console.log(descriptor.value)
return descriptor
}
return actualLogDecorator
}
class Human {
constructor(firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
@log("Logging by decorator")
getFullName() {
return this.firstName + " " + this.lastName
}
}
humanObj = new Human("Virat", "Kohli")
console.log("ngetFullName's descriptor's value property after modification due to decorator")
descriptorObject = Object.getOwnPropertyDescriptor(Human.prototype, 'getFullName')
console.log(descriptorObject.value)
console.log("nOutput for getFullName method")
console.log(humanObj.getFullName())
Steps followed by JavaScript:
var funcDescriptor = Object.getOwnPropertyDescriptor(humanObj, 'getFullName')
var actualDecorator = log("This is done using decorators")
descriptor = actualDecorator(Human.prototype, 'getFullName', funcDescriptor)
Object.defineProperty(Human.prototype, 'getFullName', descriptor);
Here, JavaScript first calls the log
method with the parameter. log
method returns the actualLogDecorator
function. actualLogDecorator
is a modified getFullName
method with logging functionality added on top of the getFullName
function.
JavaScript treats the actualLogDecorator
as the decorator function, so actualLogDecorator
returns the descriptor which has the modified getFullName
function definition.
After this, the steps followed by JavaScript to execute the getFullName
with the decorator function are same as discussed in the example of a decorator without a parameter.
Class Decorators
Class decorators are defined at the top of the class, unlike method decorators which are declared at the top of the class method. Class decorators need to return the constructor function or a new class, unlike method decorators which need to return the property descriptor. Class decorators accept only one parameter — that is the class on which they are defined. Let’s have a look at the class decorators example.
@decFunc
class Foo {
}
function Foo(FooClass) {
}
NewFoo = decFunc(Foo) || Foo
Foo = NewFoo
The above code explains the steps involved in the class decorator execution. We have an empty class Foo
with decorator function decFunc
defined on top of the class Foo
. decorFunc
accepts the class (or the constructor function in ES2015) as a parameter, modifies it and then returns the modified class (or the constructor function in ES2015) which is assigned to NewFoo
variable. The NewFoo
variable overwrites the Foo
so that now Foo
has the modified functionality, i.e. NewFoo
functionality. Let’s consider another example:
function newConstructor( HumanClass ) {
var newConstructorFunc = function(firstName, lastName, age) {
this.firstName = firstName
this.lastName = lastName
this.age = age
}
return newConstructorFunc
}
@newConstructor
class Human {
constructor( firstName, lastName ) {
this.firstName = firstName;
this.lastName = lastName;
}
}
var person1 = new Human("Virat", "Kohli", 31);
console.log( person1 );
console.log( Human.prototype.constructor );
console.log(person1.__proto__.constructor);
Here, the newConstructor
decorator is defined on the Human
class. The newConstructor
decorator returns a constructor function with firstName
lastName
and age
parameters. The returned constructor function overwrites the existing Human
constructor function. Due to this, we are able to pass the ViratKohli and 31 parameters to the Human
constructor function.
Instead of the constructor function, we could have also returned the new class itself as shown below. This NewClass
definition will overwrite the Human
class constructor function definition.
function newConstructor(HumanClass) {
return class NewClass {
constructor(firstName, lastName, age) {
this.firstName = firstName
this.lastName = lastName
this.age = age
}
}
}
Conclusion
In this article, we learned the following concepts related to decorators:
- Implementation of decorators in JavaScript using higher-order functions
- Basic of classes in JavaScript
- Property descriptors in javaScript
- Class method decorators and Class decorators in JavaScript
Source link