如何检查一个变量是否是ES6的类声明

How to check if a variable is an ES6 class declaration?

本文关键字:ES6 是否是 声明 变量 何检查 检查 一个      更新时间:2023-09-26

我正在从一个模块导出以下ES6类:

export class Thingy {
  hello() {
    console.log("A");
  }
  world() {
    console.log("B");
  }
}

并从另一个模块导入:

import {Thingy} from "thingy";
if (isClass(Thingy)) {
  // Do something...
}

如何检查变量是否为类?不是类实例,而是类声明?

换句话说,我该如何在上面的例子中实现isClass函数?

如果您想确保该值不仅是一个函数,而且是一个类的构造函数,您可以将该函数转换为字符串并检查其表示。规范规定了类构造函数的字符串表示形式。

function isClass(v) {
  return typeof v === 'function' && /^'s*class's+/.test(v.toString());
}

另一种解决方案是尝试将该值作为普通函数调用。类构造函数不能像普通函数那样调用,但不同浏览器的错误信息可能不同:

function isClass(v) {
  if (typeof v !== 'function') {
    return false;
  }
  try {
    v();
    return false;
  } catch(error) {
    if (/^Class constructor/.test(error.message)) {
      return true;
    }
    return false;
  }
}

缺点是调用函数会有各种未知的副作用…

我在这里先说清楚,任何函数都可以是构造函数。如果你区分"类"answers"函数",你就会做出糟糕的API设计选择。例如,如果您假设某些东西必须是class,那么使用Babel或Typescript的任何人都不会被检测为class,因为它们的代码将被转换为函数。这意味着你要求任何使用你的代码库的人必须在ES6环境下运行,所以你的代码在旧的环境下将无法使用。

这里的选项仅限于实现定义的行为。在ES6中,一旦代码被解析,语法被处理,就没有多少特定于类的行为了。你所拥有的只是一个构造函数。你最好的选择是做

if (typeof Thingy === 'function'){
  // It's a function, so it definitely can't be an instance.
} else {
  // It could be anything other than a constructor
}

,如果有人需要做一个非构造函数,公开一个单独的API。

显然,这不是你想要的答案,但说清楚这一点很重要。

正如这里的另一个答案所提到的,您确实有一个选项,因为函数上的.toString()需要返回一个类声明,例如

class Foo {}
Foo.toString() === "class Foo {}" // true
然而,关键的是,只有当可以时,它才适用。使用 的实现是100%符合规范的
class Foo{}
Foo.toString() === "throw SyntaxError();"

目前没有浏览器这样做,但是有几个嵌入式系统专注于JS编程,例如,为了为你的程序本身保留内存,他们丢弃源代码一旦被解析,这意味着他们将没有源代码从.toString()返回,这是允许的。

同样,通过使用.toString(),您可以对未来和通用API设计进行假设。

const isClass = fn => /^'s*class/.test(fn.toString());

因为它依赖于字符串表示,所以很容易中断。

以装饰者为例:

@decorator class Foo {}
Foo.toString() == ???

这个的.toString()包含装饰器吗?如果装饰器本身返回一个function而不是一个类呢?

检查prototype及其可写性应该允许确定函数的类型,而无需对输入进行字符串化、调用或实例化。

/**
 * determine if a variable is a class definition or function (and what kind)
 * @revised
 */
function isFunction(x) {
    return typeof x === 'function'
        ? x.prototype
            ? Object.getOwnPropertyDescriptor(x, 'prototype').writable
                ? 'function'
                : 'class'
        : x.constructor.name === 'AsyncFunction'
        ? 'async'
        : 'arrow'
    : '';
}
console.log({
  string: isFunction('foo'), // => ''
  null: isFunction(null), // => ''
  class: isFunction(class C {}), // => 'class'
  function: isFunction(function f() {}), // => 'function'
  arrow: isFunction(() => {}), // => 'arrow'
  async: isFunction(async function () {}) // => 'async'
});

这是一个古老的问题,几乎没有答案是正确的,除了这个和但是这里有一个警告…

为什么答案是错误的?

让我们从任何人建议调用函数开始…这是一种容易造成灾难的方法,应该在ChatGPT将其视为建议代码之前从建议中删除……下一个…

在JS运行时中,函数的字符串表示形式或其余代码在生产环境中不存在,可能在调试模式中,但在prod中不一定,完全相反。

这是因为一些JS运行时可以节省大量的final "字节码"通过从其几乎所有内容中删除源来缩小大小。

因此,每个建议任何字符串检查的人都不知道或考虑这些场景,而且任何函数toString方法也可以用其他东西替换,使得大多数答案不是防弹的。

为什么没有正确答案?

最接近的答案是在任意函数的prototype描述符处检查writable:

    {method(){}}这样的
  • 简写方法根本就没有原型
  • 编译成ES5函数的类可能会有可写,除非编译器非常关注这个细节,像Babel,导致运行时也稍微慢一些
  • 只有未编译的本地ES2015+代码将通过所有测试:prototype存在并且它的writable值恰好是false
const isESClass = fn => (
  typeof fn === 'function' &&
  Object.getOwnPropertyDescriptor(
    fn,
    'prototype'
  )?.writable === false
);
重要的是要明白这在ES5的项目中会失败(无论出于什么原因),但基本上没有办法保证一个泛型函数,在es2015之前的世界里,是一个类还是不是,jQuery(以及其他)使用了如下模式,所有情况都是允许的:
function jQuery(...args) {
  if (!(this instanceof jQuery))
    return new jQuery(...args);
  // do everything jQuery does
}

与ES2015+类不同的是,该实用程序既可以作为常规函数又可以作为new function,所以基本上这个问题没有正确的答案,只是需要考虑的妥协和目标列表。


为方便读者参考:

    对于常规函数,
  • 原型默认是可写的,因为MakeConstructor使用默认的
  • 类使用不可写的原型-参见第16点,MakeConstrcut被调用传递false

函数之间有细微的差别,我们可以利用这个优势来区分它们,下面是我的实现:

// is "class" or "function"?
function isClass(obj) {
    // if not a function, return false.
    if (typeof obj !== 'function') return false;
    // ⭐ is a function, has a `prototype`, and can't be deleted!
    // ⭐ although a function's prototype is writable (can be reassigned),
    //   it's not configurable (can't update property flags), so it
    //   will remain writable.
    //
    // ⭐ a class's prototype is non-writable.
    //
    // Table: property flags of function/class prototype
    // ---------------------------------
    //   prototype  write  enum  config
    // ---------------------------------
    //   function     v      .      .
    //   class        .      .      .
    // ---------------------------------
    const descriptor = Object.getOwnPropertyDescriptor(obj, 'prototype');
    // ❗functions like `Promise.resolve` do have NO `prototype`.
    //   (I have no idea why this is happening, sorry.)
    if (!descriptor) return false;
    return !descriptor.writable;
}

下面是一些测试用例:

class A { }
function F(name) { this.name = name; }
isClass(F),                 // ❌ false
isClass(3),                 // ❌ false
isClass(Promise.resolve),   // ❌ false
isClass(A),                 // ✅ true
isClass(Object),            // ✅ true

about:

function isClass(v) {
   return typeof v === 'function' && v.prototype.constructor === v;
}

这个解决方案用Felix的答案修复了两个误报:

  1. 它适用于类体前没有空格的匿名类:
    • isClass(class{}) // true
  2. 它与本地类一起工作:
    • isClass(Promise) // true
    • isClass(Proxy) // true
function isClass(value) {
  return typeof value === 'function' && (
    /^'s*class[^'w]+/.test(value.toString()) ||
    // 1. native classes don't have `class` in their name
    // 2. However, they are globals and start with a capital letter.
    (globalThis[value.name] === value && /^[A-Z]/.test(value.name))
  );
}
const A = class{};
class B {}
function f() {}
console.log(isClass(A));                // true
console.log(isClass(B));                // true
console.log(isClass(Promise));          // true
console.log(isClass(Promise.resolve));  // false
console.log(isClass(f));                // false

缺点遗憾的是,它仍然不能与node内置(可能还有许多其他特定于平台的)类一起工作,例如:
const EventEmitter = require('events');
console.log(isClass(EventEmitter));  // `false`, but should be `true` :(

来得太晚了,但如果我理解正确的话,这是另一种可以满足编译器和意图的方法。

function isInheritable(t) {
    try {
        return Boolean(class extends t {
        })
    } catch {
        return false;
    }
}

通过一些答案,并认为@Joe Hildebrand突出了边缘情况,因此以下解决方案更新以反映大多数尝试过的边缘情况。

关键观点:虽然我们正在研究类,但就像JS中指针和引用的争论一样,并没有确认其他语言的所有品质——JS本身不像其他语言结构那样有类。

有些人认为它是功能的糖衣语法,有些人则持相反的观点。我相信类在本质上仍然是一个函数,但不是像糖衣上的糖衣,而更像是一种可以加在类固醇上的东西。类可以做函数不能做的事情,或者不必费心升级它们来做的事情。

因此,暂时将类作为函数处理打开了另一个潘多拉盒子。JS中的所有东西都是对象,所有JS不理解但愿意与开发人员合作的东西都是对象,例如

  • 布尔值可以是对象(如果用new关键字定义)
  • 数字可以是对象(如果用new关键字定义)
  • 字符串可以是对象(如果用new关键字定义)
  • 日期总是对象
  • 数学永远是对象
  • 正则表达式总是对象
  • 数组总是对象
  • 函数总是对象
  • 对象始终是对象

那么类到底是什么? 重要信息类是用于创建对象的模板,在这一点上它们不是对象。当您在某处创建类的实例时,它们成为对象,该实例被认为是对象。所以我们需要筛选出

  • 我们正在处理的对象类型
  • 然后我们需要筛选它的属性。
  • 函数总是对象,它们总是有prototype和arguments属性。
  • 箭头函数实际上是老派函数的糖衣,没有这个或更多简单返回上下文的概念,所以没有原型或参数,即使你试图定义它们。
  • 类是可能的函数的蓝图,没有参数属性,但有原型。这些原型在实例之后成为事实对象。

因此,我尝试捕获并记录我们检查的每个迭代和结果。

希望有帮助

'use strict';
var isclass,AA,AAA,BB,BBB,BBBB,DD,DDD,E,F;
isclass=function(a) {
if(/null|undefined/.test(a)) return false;
    let types = typeof a;
    let props = Object.getOwnPropertyNames(a);
    console.log(`type: ${types} props: ${props}`);

    return  ((!props.includes('arguments') && props.includes('prototype')));}
    
    class A{};
    class B{constructor(brand) {
    this.carname = brand;}};
    function C(){};
     function D(a){
     this.a = a;};
 AA = A;
 AAA = new A;
 BB = B;
 BBB = new B;
 BBBB = new B('cheking');
 DD = D;
 DDD = new D('cheking');
 E= (a) => a;
 
F=class {};
 
console.log('and A is class: '+isclass(A)+''n'+'-------');
console.log('and AA as ref to A is class: '+isclass(AA)+''n'+'-------');
console.log('and AAA instance of is class: '+isclass(AAA)+''n'+'-------');
console.log('and B with implicit constructor is class: '+isclass(B)+''n'+'-------');
console.log('and BB as ref to B is class: '+isclass(BB)+''n'+'-------');
console.log('and BBB as instance of B is class: '+isclass(BBB)+''n'+'-------');
console.log('and BBBB as instance of B is class: '+isclass(BBBB)+''n'+'-------');
console.log('and C as function is class: '+isclass(C)+''n'+'-------');
console.log('and D as function method is class: '+isclass(D)+''n'+'-------');
console.log('and DD as ref to D is class: '+isclass(DD)+''n'+'-------');
console.log('and DDD as instance of D is class: '+isclass(DDD)+''n'+'-------');
console.log('and E as arrow function is class: '+isclass(E)+''n'+'-------');
console.log('and F as variable class is class: '+isclass(F)+''n'+'-------');
console.log('and isclass as variable  function is class: '+isclass(isclass)+''n'+'-------');
console.log('and 4 as number is class: '+isclass(4)+''n'+'-------');
console.log('and 4 as string is class: '+isclass('4')+''n'+'-------');
console.log('and DOMI''s string is class: '+isclass('class Im a class. Do you believe me?')+''n'+'-------');

更短的清洁函数,涵盖严格模式,es6模块,null, undefined和对象上的任何非属性操作。

到目前为止,我所发现的是,从上面的讨论中,类是蓝图,而不是在实例存在之前自己作为这样的对象。因此,运行toString函数几乎总是在实例之后产生类{}输出,而不是[object object],等等。一旦我们知道什么是一致的,然后简单地运行regex测试,看看结果是否以word class开头。

"use strict"
let isclass = a =>{
return (!!a && /^class.*{}/.test(a.toString()))
}
class A {}
class HOO {}
let B=A;
let C=new A;
Object.defineProperty(HOO, 'arguments', {
  value: 42,
  writable: false
});

console.log(isclass(A));
console.log(isclass(B));
console.log(isclass(C));
console.log(isclass(HOO));
console.log(isclass());
console.log(isclass(null));
console.log(HOO.toString());
//proxiy discussion
console.log(Proxy.toString());
//HOO was class and returned true but if we proxify it has been converted to an object
HOO = new Proxy(HOO, {});
console.log(isclass(HOO));
console.log(HOO.toString());
console.log(isclass('class Im a class. Do you believe me?'));

来自DOMI的讨论

class A {
static hello (){console.log('hello')}
hello () {console.log('hello there')}
}
A.hello();
B = new A;
B.hello();
console.log('it gets even more funnier it is properties and prototype mashing');  
class C {
  constructor() {
    this.hello = C.hello;
  }
static hello (){console.log('hello')}
}
C.say = ()=>{console.log('I said something')} 
C.prototype.shout = ()=>{console.log('I am shouting')} 
C.hello();
D = new C;
D.hello();
D.say();//would throw error as it is not prototype and is not passed with instance
C.say();//would not throw error since its property not prototype
C.shout();//would throw error as it is prototype and is passed with instance but is completly aloof from property of static 
D.shout();//would not throw error
console.log('its a whole new ball game ctaching these but gassumption is class will always have protoype to be termed as class');

我很震惊lodash竟然没有答案。看看这个-就像多米一样,我刚想出了一个解决方案来解决小故障。我知道这是很多代码,但这是目前为止我能写出的最有效、最容易理解的东西。也许有人可以通过regex方法来优化它:

function isClass(asset) {
    const string_match = "function";
    const is_fn = !!(typeof asset === string_match);
    if(!is_fn){
        return false;
    }else{
        const has_constructor = is_fn && !!(asset.prototype && asset.prototype.constructor && asset.prototype.constructor === asset);
        const code = !asset.toString ? "" : asset.toString();
        if(has_constructor && !code.startsWith(string_match)){
            return true;
        }
        if(has_constructor && code.startsWith(string_match+"(")){
            return false;
        }
        const [keyword, name] = code.split(" ");
        if(name && name[0] && name[0].toLowerCase && name[0].toLowerCase() != name[0]){
            return true;
        }else{
            return false;
        }
    }
}

测试一下:

console.log({
    _s:isClass(String),
    _a:isClass(Array),
    _o:isClass(Object),
    _c:isClass(class{}),
    fn:isClass(function(){}),
    fnn:isClass(function namedFunction(){}),
    fnc:isClass(()=>{}),
    n:isClass(null),
    o:isClass({}),
    a:isClass([]),
    s:isClass(""),
    n:isClass(2),
    u:isClass(undefined),
    b:isClass(false),
    pr:isClass(Promise),
    px:isClass(Proxy)
});

确保所有类的第一个大写字母。

也许这能帮上忙

let is_class = (obj) => {
    try {
        new obj();
        return true;
    } catch(e) {
        return false;
    };
};