为什么你需要在运行时获取类名?
\n\n想象一下:你正在用 TypeScript 编写一个复杂的系统。你有一个类的层次结构——User、Admin、Moderator。在代码的某个深处,你获取到一个对象,但不知道它到底是什么。你在控制台中只看到 object。
或者,你正在构建一个日志记录器,它不应该只输出 [Object],而应该输出有意义的类名:[User: John]。又或者,你正在编写一个装饰器,它需要根据对象的类型来决定如何处理它。
这时,Stack Overflow 上那个有上千次浏览的问题就派上用场了:"在运行时获取对象的类名"。今天,我们不仅会给出答案,还会深入 TypeScript 和 JavaScript 的底层,了解其真正的工作原理。
\n\n是的,TypeScript 是 JavaScript 的超集。所有类型的魔法在编译后都会消失。但类名不是类型,它是原型的一个属性。它在运行时仍然存在。
\n\n最简单的方法:constructor.name
\n\n如果你曾经搜索过这个问题,你很可能见过这个一行代码:
\n\nclass User {\n constructor(public name: string) {}\n}\n\nconst user = new User("Alice");\nconsole.log(user.constructor.name); // "User"\n\n有效吗?是的。快吗?是的。但如果你不了解其内部机制,有一些细节可能会让你措手不及。
\n\n底层是如何工作的?
\n\nJavaScript 中的每个对象都有一个 constructor 属性。它是对创建该对象的函数的引用。而每个函数(类也是函数)都有一个 name 属性。
当你写 user.constructor.name 时,引擎会执行以下操作:
- \n
- 沿着
user对象的原型链向上查找。 \n - 在
User.prototype上找到constructor属性。 \n - 读取构造函数
User的name属性。 \n
这是一个开销很小的操作,但并非完全没有成本。在大多数情况下,你甚至不会注意到它。
\n\n什么时候 constructor.name 会失效?
\n\n这是一个经典的陷阱:
\n\nconst obj = Object.create(null);\nconsole.log(obj.constructor); // undefined\n// console.log(obj.constructor.name); // TypeError!\n\n通过 Object.create(null) 创建的对象没有原型。它没有 constructor。请务必小心。
第二种情况是代码压缩。如果你使用打包工具(如 Webpack、Terser)重命名了类,constructor.name 可能会返回类似 t 或 e 这样的值。这仅在生产环境中是问题,但值得了解。
替代方案:instanceof 和自定义检查
\n\n有时你需要的不仅仅是名称,而是一个检查:"这个对象是类 X 的实例吗?"
\n\nclass Animal {}\nclass Dog extends Animal {}\n\nconst dog = new Dog();\n\nconsole.log(dog instanceof Dog); // true\nconsole.log(dog instanceof Animal); // true\nconsole.log(dog instanceof Object); // true\n\ninstanceof 通过原型链工作。它检查 Dog.prototype 是否存在于 dog 对象的原型链中。如果是,则返回 true。
但 instanceof 不会给你一个字符串名称。它只返回一个布尔值。对于日志记录或序列化来说,这并不适用。
组合使用:检查 + 名称
\n\n你可以编写一个工具函数,先检查类型,再返回名称:
\n\nfunction getClassName(obj: any): string | null {\n if (obj === null || obj === undefined) return null;\n if (typeof obj !== 'object' && typeof obj !== 'function') return null;\n\n // 防止 Object.create(null) 的情况\n if (!obj.constructor) return 'Object (no prototype)';\n\n const name