Obtenga el nombre de clase de la instancia de clase ES6

157

¿Hay alguna forma 'armoniosa' de obtener el nombre de clase de la instancia de clase ES6? Otro que

someClassInstance.constructor.name

Actualmente cuento con la implementación de Traceur. Y parece que Babel tiene un polyfill Function.namemientras que Traceur no.

Para resumir todo: no había otra forma en ES6 / ES2015 / Harmony, y no se espera nada ATM en ES.Next.

Puede proporcionar patrones útiles para aplicaciones no minificadas del lado del servidor, pero no es deseado en aplicaciones destinadas a navegador / escritorio / móvil.

Babel utiliza elcore-js polyfill Function.name, debe cargarse manualmente para las aplicaciones Traceur y TypeScript, según corresponda.

Frasco Estus
fuente
2
Me encontré con el mismo problema; para Traceur, la única solución fue analizar el código de clase real para extraer el nombre, lo que no creo que califique como armonioso. Me tragué la píldora y me cambié a Babel; El desarrollo de Traceur parece estar algo estancado, y muchas características de ES6 están mal implementadas. Como mencionó, instance.constructor.namey class.namedevuelva el nombre de la clase en ES6 adecuado.
Andrew Odri
Parece ser la única forma.
Frederik Krautwald
¿Está esto en el estándar ES6?
drudru
12
Vale la pena mencionar que someClassInstance.constructor.namese destrozará si uglifica su código.
JamesB
stackoverflow.com/questions/2648293/… Quizás desee ver esto, debería funcionar con / .constructor.
Florrie

Respuestas:

206

someClassInstance.constructor.namees exactamente la forma correcta de hacer esto. Los transpiladores pueden no admitir esto, pero es la forma estándar según la especificación. (La namepropiedad de las funciones declaradas a través de las producciones de ClassDeclaration se establece en 14.5.15 , paso 6.)

Domenic
fuente
2
De eso temía. ¿Conoces algún polyfill razonable para eso? Intenté descubrir cómo Babel hace eso, pero tuve muy poco éxito.
Estus Flask
Realmente no sé qué quieres decir con un polyfill para una función de lenguaje (clases).
Domenic
Me refiero al polyfill para constructor.name. Parece que Babel lo ha implementado, pero no logré entender cómo lo hace.
Estus Flask
2
@estus someClassInstance.constructores una función. Todas las funciones tienen unname propiedad que tiene su nombre. Es por eso que Babel no necesita hacer nada. Consulte developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Esteban
2
@Esteban Parece que Babel promueve persistentemente los polyfills core-js (incluido un polyfill para Function.name), es por eso que algunas compilaciones de Babel pueden funcionar de forma predeterminada en todos los navegadores.
Estus Flask
53

Como dice @Domenic, úsalo someClassInstance.constructor.name. @Esteban menciona en los comentarios que

someClassInstance.constructores una función Todas las funciones tienen una namepropiedad ...

Por lo tanto, para acceder al nombre de la clase de forma estática, haga lo siguiente (esto funciona con mi versión de Babel, por cierto. Según los comentarios en @Domenic, su kilometraje puede variar).

class SomeClass {
  constructor() {}
}

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

Actualizar

Babel estaba bien, pero uglify / minification terminó causándome problemas. Estoy haciendo un juego y estoy creando un hash de recursos de Sprite agrupados (donde la clave es el nombre de la función). Después de la minificación, cada función / clase fue nombrada t. Esto mata el hash. Estoy usando Gulpen este proyecto, y después de leer los documentos de gulp-uglify descubrí que hay un parámetro para evitar que ocurra esta alteración del nombre de la variable / función local. Entonces, en mi trago cambié

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

Aquí hay una compensación entre rendimiento y legibilidad. No alterar los nombres dará como resultado un archivo de compilación (ligeramente) más grande (más recursos de red) y una ejecución de código potencialmente más lenta (cita necesaria - puede ser BS). Por otro lado, si lo mantuviera igual, tendría que definirlo manualmente getClassNameen cada clase de ES6, a nivel estático y de instancia. ¡No, gracias!

Actualizar

Después de la discusión en los comentarios, parece que evitar la .nameconvención a favor de definir esas funciones es un buen paradigma. Solo toma unas pocas líneas de código y permitirá la minificación completa y la generalidad de su código (si se usa en una biblioteca). Así que supongo que cambio de opinión y definiré manualmente getClassNameen mis clases. Gracias @estus! . Getter / Setters suelen ser una buena idea en comparación con el acceso variable directo de todos modos, especialmente en una aplicación basada en el cliente.

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.
fuente
3
Desactivar la destrucción por completo no es una muy buena idea porque la reducción contribuye mucho a la minificación. No es una buena idea usar el tema en el código del lado del cliente en primer lugar, pero las clases se pueden proteger de la reservedopción Uglify (se puede obtener una lista de clases con regexp o lo que sea).
Estus Flas
Muy cierto. Hay una compensación de tener un archivo de mayor tamaño. Parece que así es como puede usar RegEx para evitar la manipulación de solo elementos seleccionados . ¿Qué quiere decir con que "tampoco es una buena idea usar el tema en el código del lado del cliente"? ¿Presentaría un riesgo de seguridad en algunos escenarios?
James L.
1
No, justo lo que ya se dijo. Es normal que se minimice el JS del lado del cliente, por lo que ya se sabe que este patrón causará problemas a la aplicación. Una línea de código adicional para el identificador de cadena de clase en lugar de un namepatrón ordenado puede ser simplemente ganar-ganar. Lo mismo se puede aplicar a Node, pero en menor medida (por ejemplo, la aplicación de Electron ofuscado). Como regla general, confiaría nameen el código del servidor, pero no en el código del navegador y no en la biblioteca común.
Estus Flask
OK, así que está recomendando definir manualmente 2 funciones getClassName (estática e instancia) para evitar el infierno y permitir una minificación completa (sin un RegEx molesto). Ese punto sobre la biblioteca tiene mucho sentido. Tiene mucho sentido. Para mí, mi proyecto es una pequeña aplicación Cordova hecha a sí misma, por lo que no son realmente problemas. Más allá de eso, puedo ver surgir estos problemas. Gracias por la discusión! Si puede pensar en alguna mejora en la publicación, no dude en editarla.
James L.
1
Sí, inicialmente nameutilicé el código DRYer para extraer el nombre de la entidad que usa la clase (servicio, complemento, etc.) del nombre de la clase, pero al final descubrí que lo duplica explícitamente con el accesorio estático ( id, _name) Es el enfoque más sólido. Una buena alternativa es no preocuparse por el nombre de la clase, usar una clase en sí misma (objeto de función) como un identificador para una entidad que está asignada a esta clase y importcuando sea necesario (un enfoque que fue utilizado por Angular 2 DI).
Estus Flas
8

Obtener el nombre de la clase directamente de la clase

Las respuestas anteriores explicaron que someClassInstance.constructor.namefunciona bien, pero si necesita convertir mediante programación el nombre de la clase en una cadena y no desea crear una instancia solo para eso, recuerde que:

typeof YourClass === "function"

Y, dado que cada función tiene una namepropiedad, otra buena manera de obtener una cadena con el nombre de su clase es simplemente hacer:

YourClass.name

Lo que sigue es un buen ejemplo de por qué esto es útil.

Cargando componentes web

Como nos enseña la documentación de MDN , así es como carga un componente web:

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

¿ YourComponentDesde dónde se extiende una clase HTMLElement? Dado que se considera una buena práctica nombrar la clase de su componente después de la etiqueta del componente en sí, sería bueno escribir una función auxiliar que todos sus componentes puedan usar para registrarse. Así que aquí está esa función:

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

Entonces, todo lo que necesitas hacer es:

registerComponent(YourComponent);

Lo cual es bueno porque es menos propenso a errores que escribir la etiqueta del componente usted mismo. Para concluir, esta es la upperCamelCaseToSnakeCase()función:

// 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
fuente
Gracias. El ejemplo es del lado del cliente. Como ya se mencionó, hay algunos problemas con el uso de nombres de funciones en los navegadores. Se espera que casi todos los fragmentos de código del navegador se minimicen, pero esto arruinará el código que depende del nombre de la función.
Estus Flas
Sí, tienes toda la razón. El minificador tendría que estar configurado para no tocar los nombres de clase para que este enfoque funcione.
Lucio Paiva
1

Para la transpilación de babel (antes de la minificación)

Si está utilizando Babel con @babel/preset-env, es posible mantener las definiciones de clases sin convertirlas en funciones (lo que elimina elconstructor propiedad)

Puede eliminar cierta compatibilidad del navegador anterior con esta configuración en su babel.config / babelrc:

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

Más información sobre targets: https://babeljs.io/docs/en/babel-preset-env#targets

Para la minificación de babel (después de la transpilación)

Parece que no hay una solución fácil en este momento ... Necesitamos analizar las exclusiones.

Si no
fuente
¿Puede explicar más cómo se supone que esto ayuda con la minificación? class Foo {}se reducirá a algo parecido class a{}a cualquier objetivo. No habrá Foopalabra en el código fuente minificado.
Estus Flask
Honestamente, no excavé más que las documentaciones y el hecho de que me ayudó a usar esta configuración ... Estoy usando ECSY en el proyecto transpilado de Babel y requiere este parámetro para obtener nombres de clase válidos: github.com/MozillaReality/ecsy/issues/ 119
Si no
Veo. Esto es muy específico para el código que trató. Por ejemplo, los nombres pueden conservarse para los módulos ES y ES6 porque export class Foo{}no se pueden uglificar más eficientemente, pero esto puede ser diferente en otros lugares, no podemos saber exactamente sin tener una buena idea de lo que sucede con piezas de código específicas en el momento de la compilación. De cualquier manera, esto no ha cambiado desde 2015. Siempre fue posible para algunas configuraciones y código de compilación. Y diría que esta posibilidad aún es demasiado frágil para usar nombres de clase para la lógica de la aplicación. El nombre de la clase en la que confía puede convertirse en basura después de un cambio accidental en el código fuente
Estus Flask
1
Ok, encontré lo que está sucediendo mirando el código. Mi solución corrige la transpilación de clases a funciones. Por lo tanto, ayuda antes de la minificación. Pero no con el problema de la minificación. Tengo que seguir cavando porque no puedo entender cómo funciona todo mi código constructor.nameen la versión minimizada ... ilógico: /
Si no