获取ES6类实例的类名

196

除了之外,有没有从ES6类实例获取类名的“和谐”方法?

someClassInstance.constructor.name

目前我正在依赖Traceur实现。而Babel似乎有一个用于Function.name的polyfill,而Traceur没有。

总之:在ES6/ES2015/Harmony中没有其他方法,在ES.Next中也没有预期任何变化。

它可能为未压缩的服务器端应用程序提供有用的模式,但对于用于浏览器/桌面/移动设备的应用程序而言是不需要的。

Babel 使用core-js来polyfillFunction.name,它应该根据需要手动加载到Traceur和TypeScript应用程序中。


2
我遇到了同样的问题;对于Traceur,唯一的解决方案是解析实际的类代码本身来提取名称,我认为这并不算是和谐的解决方式。我只好咽下苦果,转而使用Babel;Traceur的发展似乎有些停滞不前,许多ES6特性实现得很差。正如你所提到的,instance.constructor.nameclass.name可以正确返回ES6中的类名。 - Andrew Odri
似乎这是唯一的方法。 - Frederik Krautwald
这是否符合ES6标准? - drudru
20
值得一提的是,如果您使用uglify压缩代码,则someClassInstance.constructor.name可能会被修改。 - JamesB
可能需要查看这个链接,应该可以使用.constructor来获取函数名。 - Nebula
5个回答

251

someClassInstance.constructor.name 是准确的方法。虽然转译器可能不支持此方法,但它是规范中的标准方式。(通过 ClassDeclaration 生成的函数的 name 属性在 14.5.15 的第6步中设置。)


2
这正是我担心的。你知道有什么合理的polyfill吗?我尝试了解Babel是如何做到的,但成功率很低。 - Estus Flask
我不太清楚你所说的语言特性(类)的 polyfill 是什么意思。 - Domenic
我在支持基本类的Firefox Nightly上尝试,但它返回字符串“constructor”而不是我的类名“Hello” :( - Zorgatone
2
@estus someClassInstance.constructor 是一个函数。所有的函数都有一个名为 name 的属性,该属性设置为其名称。这就是为什么 Babel 不需要做任何事情的原因。请参阅 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name - Esteban
2
@Esteban 看起来Babel一直在推广core-js polyfills(包括Function.name的polyfill),这就是为什么一些Babel构建可以在所有浏览器中开箱即用的原因。 - Estus Flask
显示剩余3条评论

66

正如@Domenic所说,使用someClassInstance.constructor.name。 @Esteban在评论中提到:

someClassInstance.constructor是一个函数。所有函数都有一个name属性...

因此,要静态访问类名,请执行以下操作(这适用于我的Babel版本。根据@Domenic的评论,您的里程可能会有所不同)。

class SomeClass {
  constructor() {}
}

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

更新

Babel没问题,但是uglify/minification确实给我带来了问题。我在做一个游戏,正在创建一个池化精灵资源的哈希表(其中关键字是函数名)。经过缩小后,每个函数/类都被命名为t。这破坏了哈希表。我在这个项目中使用Gulp,在阅读gulp-uglify文档后,我发现有一个参数可以防止局部变量/函数名混淆。所以,在我的gulpfile中,我将

.pipe($.uglify())更改为.pipe($.uglify({ mangle: false }))

这里存在性能与可读性之间的权衡。不混淆名称将导致构建文件略微变大(更多网络资源),并可能使代码执行变慢(需要引证-可能是胡说八道)。另一方面,如果我保持不变,我将不得不在每个ES6类的静态和实例级别上手动定义getClassName。不要!

更新

在评论中的讨论后,避免使用.name约定而选择定义那些函数是一个好的范例。这只需要几行代码,将允许完全缩小和通用性的代码(如果在库中使用)。所以我改变了主意,会在我的类上手动定义getClassName。谢谢@estus!。与直接变量访问相比,Getter/Setters通常是一个好主意,特别是在客户端应用程序中。

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)

3
完全禁用名称混淆并不是一个很好的主意,因为名称混淆对于代码的缩小起了很大的作用。在客户端代码中使用该对象也不是很好的主意,但可以使用“保留”Uglify选项来保护类免受名称混淆(可以使用正则表达式或其他方式获取类列表)。 - Estus Flask
非常正确。拥有更大的文件大小是一种权衡。看起来这就是您可以使用RegEx仅防止选定项目混淆的方法。您说“在客户端代码中使用主题也不是一个很好的想法”,这是什么意思?在某些情况下,它会带来安全风险吗? - James L.
1
不,只是已经说过的内容。客户端JS被压缩是很正常的,因此已经知道这种模式会对应用程序造成问题。为了获得类字符串标识符,额外添加一行代码而不是整洁的“名称”模式可能只是双赢。同样的事情也可以应用于Node,但程度较小(例如混淆的Electron应用程序)。作为经验法则,我会在服务器代码中依赖“名称”,但不会在浏览器代码和常见库中使用它。 - Estus Flask
好的,所以你建议手动定义2个getClassName函数(静态和实例)来避免混淆问题并允许完全的最小化(无需烦人的正则表达式)。那个关于库的观点很有道理。很有道理。对我来说,我的项目是一个自制的小型Cordova应用程序,所以这些并不是真正的问题。除此之外,我可以看到这些问题可能会出现。谢谢讨论!如果您能想到任何改进帖子的方法,请随意编辑它。 - James L.
1
是的,我最初使用 name 来提取使用类(服务、插件等)的实体的名称,以使代码更加DRY,但最终我发现明确地使用静态属性(id, _name)来重复它才是最可靠的方法。一个很好的替代方案是不关心类名,而是使用类本身(函数对象)作为映射到该类的实体的标识符,并在需要的地方进行import(这是 Angular 2 DI 使用的方法)。 - Estus Flask

16

从类直接获取类名

之前的答案已经解释了someClassInstance.constructor.name可以正常工作,但是如果你需要以编程方式将类名转换为字符串而且不想为此创建实例,请记住:

typeof YourClass === "function"

而且,由于每个函数都有一个name属性,获取带有您类名称的字符串的另一种好方法只需要执行以下操作:

YourClass.name

接下来的内容是为什么这很有用的一个很好的例子。

加载 Web 组件

正如MDN 文档所教导的,这是如何加载 Web 组件的:

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

在这里,YourComponent 是一个继承自 HTMLElement 的类。由于按照组件标签命名组件类被认为是一种良好的实践,因此编写一个帮助函数以注册所有组件将会很好。以下是该函数:

function registerComponent(componentClass) {
    const componentName = upperCamelCaseToSnakeCase(componentClass.name);
    customElements.define(componentName, componentClass);
}
所以您需要做的就是:
registerComponent(YourComponent);

这很好,因为这比自己编写组件标记少出错。总之,这就是 upperCamelCaseToSnakeCase() 函数:

// 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());
}

谢谢。这个例子是客户端的。正如已经提到的,使用浏览器中的函数名称存在一些问题。几乎每个浏览器代码片段都应该被压缩,但这将破坏依赖于函数名称的代码。 - Estus Flask
是的,你完全正确。为了使这种方法起作用,缩小器必须被配置为不触及类名。 - Lucio Paiva

2

针对 Babel 转译(在缩小之前)

如果你正在使用带有 @babel/preset-env 的 Babel,那么可以保留类定义而不将它们转换为函数(这会删除 constructor 属性)。

你可以在你的 babel.config / babelrc 中使用以下配置来放弃一些旧的浏览器兼容性:

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

针对Babel压缩(在转译之后)

目前看起来似乎没有简单的解决方案......我们需要查看混淆排除项。

有关 targets 的更多信息:https://babeljs.io/docs/en/babel-preset-env#targets


你能进一步解释这如何帮助缩小文件大小吗?无论目标是什么,class Foo {}都将被缩小为类似于class a{}的形式。在缩小后的源代码中不会出现Foo这个单词。 - Estus Flask
说实话,我没有深入研究文档,只是发现这个配置对我有帮助... 我正在将 ECSY 用于 Babel 转译的项目中,并且需要使用此参数来获取有效的类名:https://github.com/MozillaReality/ecsy/issues/119 - Ifnot
我明白了。这与你处理的代码非常相关。例如,对于ES模块和ES6,名称可能会被保留,因为export class Foo{}无法进一步有效地混淆,但在其他地方可能会有所不同,如果没有一个好的想法来了解特定代码片段在构建时发生了什么,我们就无法确切知道。无论如何,自2015年以来,这种情况并没有改变。对于某些编译配置和代码,这始终是可能的。我认为,使用类名作为应用程序逻辑仍然太脆弱了。您依赖的类名可能会在源代码中发生意外更改后变成垃圾。 - Estus Flask
1
好的,我通过查看代码找到了问题所在。我的解决方案修复了类到函数的转换问题。因此,在缩小之前可以帮助解决问题。但是无法解决缩小问题。我必须继续挖掘,因为我不明白为什么所有使用“constructor.name”的代码在缩小版本中仍然有效...这很不合逻辑 :/ - Ifnot

1

如果您掌握类的实现

我在谷歌搜索时发现了这篇文章,它可以帮助您解决问题。

以下内容适用于Node.js环境。

./SuperClass.js

'use strict';

/**
 * A super class
 *
 * To show that it works with inheritance too
 *
 * In my usage, I'm also extending the Error class
 */
module.exports = class SuperClass extends Error {
    constructor(message, options = {}) {
        super(message);
        this.name = this.constructor.name;
        // ...other shared construction functionality
    }
};

./SubClass.js

'use strict';

const SuperClass = require('./SuperClass');
/**
 * a sub class
 */
module.exports = class SubClass extends SuperClass {
    constructor(message, options = {}) {
        super(message, options);
        // ... specific sub class construction functionality
    }
};

SubClass.name; // -> 'SubClass'
let instance = new SubClass('message');
instance.name; // -> 'SubClass'

显然,这只适用于您完全控制实现的类。

我可能会将其更改为_name或其他内容,但这是为了使其与我正在使用的另一个库保持一致。


1
由于name实例属性不会干扰构造函数属性,我在这里看不到问题。请注意,像前端应用程序一样,name仍然是缩小代码的问题 https://dev59.com/6F0b5IYBdhLWcg3wVv-9#39522406 - Estus Flask
谢谢你提醒我。说实话,我正在开发的项目还没有投入生产。我会记住这个问题,如果我压缩/混淆应用程序并开始遇到任何问题,我就知道该去哪里查找。 - DazBaldwin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接