Obtenez le nom de classe de l'instance de classe ES6

157

Existe-t-il des moyens «harmonieux» pour obtenir le nom de classe à partir de l'instance de classe ES6? Autre que

someClassInstance.constructor.name

Actuellement je compte sur l'implémentation de Traceur. Et il semble que Babel ait un polyfill pendant Function.nameque Traceur n'en a pas.

Pour résumer le tout: il n'y avait pas d'autre moyen dans ES6 / ES2015 / Harmony, et rien n'est attendu ATM dans ES.Next.

Il peut fournir des modèles utiles pour les applications côté serveur non réduites mais n'est pas souhaité dans les applications destinées au navigateur / bureau / mobile.

Babel utilisecore-js pour polyfill Function.name, il doit être chargé manuellement pour les applications Traceur et TypeScript selon le cas.

Fiole d'Estus
la source
2
Je suis tombé sur le même problème; pour Traceur, la seule solution était d'analyser le code de classe lui-même pour en extraire le nom, ce que je ne pense pas être considéré comme harmonieux. J'ai juste avalé la pilule et je suis passé à Babel; Le développement de Traceur semble être quelque peu stagnant et de nombreuses fonctionnalités ES6 sont mal implémentées. Comme vous l'avez mentionné, instance.constructor.nameet class.namerenvoyez le nom de la classe dans ES6 approprié.
Andrew Odri
Semble être le seul moyen.
Frederik Krautwald
Est-ce dans la norme ES6?
drudru
12
Il vaut la peine de mentionner que cela someClassInstance.constructor.namesera mutilé si vous uglifiez votre code.
JamesB
stackoverflow.com/questions/2648293/... Pourrait vouloir regarder ceci, devrait fonctionner avec .constructor.
Florrie

Réponses:

206

someClassInstance.constructor.nameest exactement la bonne façon de procéder. Les transpileurs peuvent ne pas prendre en charge cela, mais c'est la méthode standard selon la spécification. (La namepropriété des fonctions déclarées via les productions ClassDeclaration est définie dans 14.5.15 , étape 6.)

Domenic
la source
2
C'est ce dont j'avais peur. Connaissez-vous un polyfill raisonnable pour cela? J'ai essayé de comprendre comment Babel fait cela mais j'ai eu très peu de succès.
Estus Flask
Je ne sais pas vraiment ce que vous entendez par polyfill pour une fonctionnalité de langage (classes).
Domenic
Je veux dire le polyfill pour constructor.name. Il semble que Babel l'ait mis en œuvre, mais je n'ai pas réussi à comprendre comment il le fait.
Estus Flask
2
@estus someClassInstance.constructorest une fonction. Toutes les fonctions ont une namepropriété définie sur son nom. C'est pourquoi Babel n'a rien à faire. Veuillez consulter developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Esteban
2
@Esteban Il semble que Babel encourage constamment les polyfills core-js (y compris un polyfill pour Function.name), c'est pourquoi certaines versions de Babel peuvent fonctionner directement dans tous les navigateurs.
Estus Flask
53

Comme le dit @Domenic, utilisez someClassInstance.constructor.name. @Esteban mentionne dans les commentaires que

someClassInstance.constructorest une fonction. Toutes les fonctions ont une namepropriété ...

Ainsi, pour accéder au nom de la classe de façon statique, procédez comme suit (cela fonctionne avec ma version Babel BTW. Selon les commentaires sur @Domenic votre kilométrage peut varier).

class SomeClass {
  constructor() {}
}

var someClassInstance = new SomeClass();
someClassInstance.constructor.name;      // === 'SomeClass'
SomeClass.name                           // === 'SomeClass'

Mettre à jour

Babel allait bien, mais uglify / minification a fini par me causer des problèmes. Je crée un jeu et je crée un hachage de ressources Sprite mises en commun (où la clé est le nom de la fonction). Après la minification, chaque fonction / classe a été nommée t. Cela tue le hachage. J'utilise Gulpdans ce projet, et après avoir lu la documentation gulp-uglify, j'ai découvert qu'il y avait un paramètre pour empêcher cette modification du nom de la variable / fonction locale. Donc, dans mon gulpfile j'ai changé

.pipe($.uglify()) à .pipe($.uglify({ mangle: false }))

Il y a ici un compromis entre performances et lisibilité. Ne pas modifier les noms entraînera un fichier de construction (légèrement) plus volumineux (plus de ressources réseau) et une exécution de code potentiellement plus lente (citation nécessaire - peut être BS). D'un autre côté, si je gardais la même chose, je devrais définir manuellement getClassNamesur chaque classe ES6 - au niveau statique et de l'instance. Non merci!

Mettre à jour

Après la discussion dans les commentaires, il semble qu'éviter la .nameconvention en faveur de la définition de ces fonctions soit un bon paradigme. Cela ne prend que quelques lignes de code et permettra une minification complète et une généralisation de votre code (s'il est utilisé dans une bibliothèque). Donc je suppose que je change d'avis et que je définirai manuellement getClassNamemes cours. Merci @estus! . Les Getter / Setters sont généralement une bonne idée par rapport à l'accès direct aux variables de toute façon, en particulier dans une application client.

class SomeClass {
  constructor() {}
  static getClassName(){ return 'SomeClass'; }
  getClassName(){ return SomeClass.getClassName(); }
}
var someClassInstance = new SomeClass();
someClassInstance.constructor.getClassName();      // === 'SomeClass' (static fn)
someClassInstance.getClassName();                  // === 'SomeClass' (instance fn)
SomeClass.getClassName()                           // === 'SomeClass' (static fn)
James L.
la source
3
Désactiver complètement la mutilation n'est pas une très bonne idée car la mutilation contribue beaucoup à la minification. Ce n'est pas non plus une très bonne idée d'utiliser le sujet dans le code côté client en premier lieu, mais les classes peuvent être protégées de la déformation avec l' reservedoption Uglify (une liste de classes peut être obtenue avec regexp ou autre).
Estus Flask
Très vrai. Il y a un compromis à avoir une taille de fichier plus grande. On dirait que c'est ainsi que vous pouvez utiliser RegEx pour éviter de modifier uniquement certains éléments . Que voulez-vous dire par "n'est pas non plus une très bonne idée d'utiliser le sujet dans le code côté client"? Présenterait-il un risque de sécurité dans certains scénarios?
James L.
1
Non, juste ce qui a déjà été dit. Il est normal que JS côté client soit minifié, il est donc déjà connu que ce modèle causera des problèmes pour l'application. Une ligne de code supplémentaire pour l'identificateur de chaîne de classe au lieu d'un namemodèle soigné peut simplement être gagnant-gagnant. La même chose peut être appliquée à Node, mais dans une moindre mesure (par exemple, application Electron masquée). En règle générale, je me fierais nameau code serveur, mais pas au code navigateur ni à la bibliothèque commune.
Estus Flask
OK, vous recommandez donc de définir manuellement 2 fonctions getClassName (statique et instance) pour contourner l'enfer et permettre une minification complète (sans RegEx ennuyeux). Ce point concernant la bibliothèque a beaucoup de sens. Cela a beaucoup de sens. Pour moi, mon projet est une petite application Cordova, donc ce ne sont pas vraiment des problèmes. Au-delà de cela, je peux voir ces problèmes surgir. Merci pour la discussion! Si vous pouvez penser à des améliorations de l'article, n'hésitez pas à le modifier.
James L.
1
Oui, j'ai initialement utilisé le namecode DRYer pour extraire le nom de l'entité qui utilise la classe (service, plugin, etc.) du nom de la classe, mais à la fin, j'ai trouvé qu'il le dupliquait explicitement avec static prop ( id, _name) est juste l'approche la plus solide. Une bonne alternative est de ne pas se soucier du nom de la classe, d'utiliser une classe elle-même (objet fonction) comme identifiant pour une entité mappée à cette classe et importlà où c'est nécessaire (une approche qui a été utilisée par Angular 2 DI).
Estus Flask
8

Obtenir le nom de la classe directement depuis la classe

Les réponses précédentes ont expliqué que cela someClassInstance.constructor.namefonctionne très bien, mais si vous devez convertir par programme le nom de la classe en une chaîne et que vous ne voulez pas créer une instance juste pour cela, rappelez-vous que:

typeof YourClass === "function"

Et, puisque chaque fonction a une namepropriété, une autre manière intéressante d'obtenir une chaîne avec le nom de votre classe est de simplement faire:

YourClass.name

Ce qui suit est un bon exemple de pourquoi cela est utile.

Chargement des composants Web

Comme nous l'enseigne la documentation MDN, voici comment charger un composant Web:

customElements.define("your-component", YourComponent);

D'où YourComponentvient une classe qui s'étend HTMLElement. Puisqu'il est considéré comme une bonne pratique de nommer la classe de votre composant après la balise de composant elle-même, il serait bien d'écrire une fonction d'assistance que tous vos composants pourraient utiliser pour s'enregistrer. Voici donc cette fonction:

function registerComponent(componentClass) {
    const componentName = upperCamelCaseToSnakeCase(componentClass.name);
    customElements.define(componentName, componentClass);
}

Il vous suffit donc de:

registerComponent(YourComponent);

Ce qui est bien car c'est moins sujet aux erreurs que d'écrire vous-même la balise de composant. Pour conclure, voici la upperCamelCaseToSnakeCase()fonction:

// converts `YourString` into `your-string`
function upperCamelCaseToSnakeCase(value) {
    return value
        // first char to lower case
        .replace(/^([A-Z])/, $1 => $1.toLowerCase())
        // following upper chars get preceded with a dash
        .replace(/([A-Z])/g, $1 => "-" + $1.toLowerCase());
}
Lucio Paiva
la source
Merci. L'exemple est côté client. Comme cela a déjà été mentionné, l'utilisation des noms de fonction dans les navigateurs pose certains problèmes. On s'attend à ce que presque tous les morceaux de code du navigateur soient minifiés, mais cela ruinera le code qui repose sur le nom de la fonction.
Estus Flask
Oui, vous avez tout à fait raison. Le minificateur devrait être configuré pour ne pas toucher les noms de classe pour que cette approche fonctionne.
Lucio Paiva
1

Pour la transpilation babel (avant minification)

Si vous utilisez Babel avec @babel/preset-env, il est possible de conserver les définitions de classes sans les convertir en fonctions (ce qui supprime la constructorpropriété)

Vous pouvez supprimer une ancienne compatibilité de navigateur avec cette configuration dans votre babel.config / babelrc:

{
  "presets": [
    ["@babel/preset-env", {"targets": {"browsers": ["> 2%"]}}]
  ]
}

Plus d'informations sur targets: https://babeljs.io/docs/en/babel-preset-env#targets

Pour la minification de babel (après transpilation)

Il semble qu'il n'y a pas de solution facile pour le moment ... Nous devons envisager de réduire les exclusions.

Si non
la source
Pouvez-vous expliquer plus en détail comment cela est censé aider à la minification? class Foo {}sera réduit à quelque chose comme class a{}avec n'importe quelle cible. Il n'y aura aucun Foomot dans le code source minifié.
Estus Flask
Honnêtement, je n'ai pas creusé plus que les documentations et le fait que cela m'a aidé à utiliser cette configuration ... J'utilise ECSY dans le projet transpiled babel et il a besoin de ce paramètre pour obtenir des noms de classe valides: github.com/MozillaReality/ecsy/issues/ 119
Ifnot
Je vois. Ceci est très spécifique au code que vous avez traité. Par exemple, les noms peuvent être préservés pour les modules ES et ES6 car export class Foo{}ils ne peuvent pas être davantage modifiés de manière efficace, mais cela peut être différent à d'autres endroits, nous ne pouvons pas savoir exactement comment sans avoir une bonne idée de ce qui se passe avec des morceaux de code spécifiques au moment de la construction. Quoi qu'il en soit, cela n'a pas changé depuis 2015. C'était toujours possible pour certaines configurations de compilation et du code. Et je dirais que cette possibilité est encore trop fragile pour utiliser des noms de classe pour la logique d'application. Le nom de classe sur lequel vous comptez peut devenir une poubelle après un changement accidentel du code source
Estus Flask
1
Ok j'ai trouvé ce qui se passe en regardant le code. Ma solution corrige la transpilation des classes en fonctions. Cela aide donc avant la minification. Mais pas avec le problème de la minification. Je dois continuer à creuser car, je ne pouvais pas comprendre comment tout mon code en utilisant constructor.namefonctionne toujours dans la version minifiée ... illogique: /
Si non