How do you check the difference between an ECMAScript 6 class and function?

10,978

Solution 1

I think the simplest way to check if the function is ES6 class is to check the result of .toString() method. According to the es2015 spec:

The string representation must have the syntax of a FunctionDeclaration FunctionExpression, GeneratorDeclaration, GeneratorExpression, ClassDeclaration, ClassExpression, ArrowFunction, MethodDefinition, or GeneratorMethod depending upon the actual characteristics of the object

So the check function looks pretty simple:

function isClass(func) {
  return typeof func === 'function' 
    && /^class\s/.test(Function.prototype.toString.call(func));
}

Solution 2

I did some research and found out that the prototype object [spec 19.1.2.16] of ES6 classes seem to be non-writeable, non-enumerable, non-configurable.

Here's a way to check:

class F { }

console.log(Object.getOwnPropertyDescriptor(F, 'prototype'));
// {"value":{},"writable":false,"enumerable":false,"configurable":false

A regular function by default is writeable, non-enumerable, non-configurable.

function G() { }

console.log(Object.getOwnPropertyDescriptor(G, 'prototype'));
// {"value":{},"writable":true,"enumerable":false,"configurable":false}

ES6 Fiddle: http://www.es6fiddle.net/i7d0eyih/

So an ES6 class descriptor will always have those properties set to false and will throw an error if you try to define the descriptors.

// Throws Error
Object.defineProperty(F, 'prototype', {
  writable: true
});

However with a regular function you can still define those descriptors.

// Works
Object.defineProperty(G, 'prototype', {
  writable: false
});

It's not very common that descriptors are modified on regular functions so you can probably use that to check if it's a class or not, but of course this is not a real solution.

@alexpods' method of stringifying the function and checking for the class keyword is probably the the best solution at the moment.

Solution 3

Since the existing answers address this problem from an ES5-environment perspective I thought it might be worth offering an answer from an ES2015+ perspective; the original question doesn’t specify and today many people no longer need to transpile classes, which alters the situation a bit.

In particular I wanted to note that it is possible to definitively answer the question "can this value be constructed?" Admittedly, that’s not usually useful on its own; the same fundamental problems continue to exist if you need to know if a value can be called.

Is something constructable?

To start I think we need to clarify some terminology because asking whether a value is a constructor can mean more than one thing:

  1. Literally, does this value have a [[construct]] slot? If it does, it is constructable. If it does not, it is not constructable.
  2. Was this function intended to be constructed? We can produce some negatives: functions that cannot be constructed were not intended to be constructed. But we can’t also say (without resorting to heuristic checks) whether a function which is constructable wasn’t meant to used as a constructor.

What makes 2 unanswerable is that functions created with the function keyword alone are both constructable and callable, but such functions are often intended for only one of these purposes. As some others have mentioned, 2 is also a fishy question — it is akin to asking "what was the author thinking when they wrote this?" I don’t think AI is there yet :) While in a perfect world perhaps all authors would reserve PascalCase for constructors (see balupton’s isConventionalClass function), in practice it would not be unusual to encounter false positives/negatives with this test.

Regarding the first version of this question, yes, we can know if a function is constructable. The obvious thing to do is to try constructing it. This isn’t really acceptable though because we don’t know if doing so would have side effects — it seems like a given that we don’t know anything about the nature of the function, since if we did, we wouldn’t need this check). Fortunately there is a way to construct a constructor without really constructing it:

const isConstructable = fn => {
  try {
    new new Proxy(fn, { construct: () => ({}) });
    return true;
  } catch (err) {
    return false;
  }
};

The construct Proxy handler can override a proxied value’s [[construct]], but it cannot make a non constructable value constructable. So we can "mock instantiate" the input to test whether this fails. Note that the construct trap must return an object.

isConstructable(class {});                      // true
isConstructable(class {}.bind());               // true
isConstructable(function() {});                 // true
isConstructable(function() {}.bind());          // true
isConstructable(() => {});                      // false
isConstructable((() => {}).bind());             // false
isConstructable(async () => {});                // false
isConstructable(async function() {});           // false
isConstructable(function * () {});              // false
isConstructable({ foo() {} }.foo);              // false
isConstructable(URL);                           // true

Notice that arrow functions, async functions, generators and methods are not double-duty in the way "legacy" function declarations and expressions are. These functions are not given a [[construct]] slot (I think not many realize that "shorthand method" syntax is does something — it’s not just sugar).

So to recap, if your question is really "is this constructable," the above is conclusive. Unfortunately nothing else will be.

Is something callable?

We’ll have to clarify the question again, because if we’re being very literal, the following test actually works*:

const isCallable = fn => typeof fn === 'function';

This is because ES does not currently let you create a function without a [[call]] slot (well, bound functions don’t directly have one, but they proxy down to a function that does).

This may seem untrue because constructors created with class syntax throw if you try to call them instead of constructing them. However they are callable — it’s just that their [[call]] slot is defined as a function that throws! Oy.

We can prove this by converting our first function to its mirror image.

// Demonstration only, this function is useless:

const isCallable = fn => {
  try {
    new Proxy(fn, { apply: () => undefined })();
    return true;
  } catch (err) {
    return false;
  }
};

isCallable(() => {});                      // true
isCallable(function() {});                 // true
isCallable(class {});                      // ... true!

Such a function is not helpful, but I wanted to show these results to bring the nature of the problem into focus. The reason we can’t easily check whether a function is "new-only" is that the answer is not modeled in terms of "absence of call" the way "never-new" is modeled in terms of "absence of construct". What we’re interested in knowing is buried in a method we cannot observe except through its evaluation, so all that we can do is use heuristic checks as a proxy for what we really want to know.

Heuristic options

We can begin by narrowing down the cases which are ambiguous. Any function which is not constructable is unambiguously callable in both senses: if typeof fn === 'function' but isConstructable(fn) === false, we have a call-only function such as an arrow, generator, or method.

So the four cases of interest are class {} and function() {} plus the bound forms of both. Everything else we can say is only callable. Note that none of the current answers mention bound functions, but these introduce significant problems to any heuristic check.

As balupton points out, the presence or absence of a property descriptor for the 'caller' property can act as an indicator of how a function was created. A bound function exotic object will not have this own-property even if the function it wraps does. The property will exist via inheritence from Function.prototype, but this is true also for class constructors.

Likewise, toString for a BFEO will normally begin 'function' even if the bound function was created with class. Now, a heuristic for detecting BFEOs themselves would be to see if their name begins 'bound ', but unfortunately this is a dead end; it still tells us nothing about what was bound — this is opaque to us.

However if toString does return 'class' (which won’t be true for e.g. DOM constructors), that’s a pretty solid signal that it’s not callable.

The best we can do then is something like this:

const isDefinitelyCallable = fn =>
  typeof fn === 'function' &&
  !isConstructable(fn);

isDefinitelyCallable(class {});                      // false
isDefinitelyCallable(class {}.bind());               // false
isDefinitelyCallable(function() {});                 // false <-- callable
isDefinitelyCallable(function() {}.bind());          // false <-- callable
isDefinitelyCallable(() => {});                      // true
isDefinitelyCallable((() => {}).bind());             // true
isDefinitelyCallable(async () => {});                // true
isDefinitelyCallable(async function() {});           // true
isDefinitelyCallable(function * () {});              // true
isDefinitelyCallable({ foo() {} }.foo);              // true
isDefinitelyCallable(URL);                           // false

const isProbablyNotCallable = fn =>
  typeof fn !== 'function' ||
  fn.toString().startsWith('class') ||
  Boolean(
    fn.prototype &&
    !Object.getOwnPropertyDescriptor(fn, 'prototype').writable // or your fave
  );

isProbablyNotCallable(class {});                      // true
isProbablyNotCallable(class {}.bind());               // false <-- not callable
isProbablyNotCallable(function() {});                 // false
isProbablyNotCallable(function() {}.bind());          // false
isProbablyNotCallable(() => {});                      // false
isProbablyNotCallable((() => {}).bind());             // false
isProbablyNotCallable(async () => {});                // false
isProbablyNotCallable(async function() {});           // false
isProbablyNotCallable(function * () {});              // false
isProbablyNotCallable({ foo() {} }.foo);              // false
isProbablyNotCallable(URL);                           // true

The cases with arrows point out where we get answers we don’t particularly like.

In the isProbablyNotCallable function, the last part of the condition could be replaced with other checks from other answers; I chose Miguel Mota’s here, since it happens to work with (most?) DOM constructors as well, even those defined before ES classes were introduced. But it doesn’t really matter — each possible check has a downside and there is no magic combo.


The above describes to the best of my knowledge what is and isn’t possible in contemporary ES. It doesn’t address needs that are specific to ES5 and earlier, though really in ES5 and earlier the answer to both of the questions is always "true" for any function.

The future

There is a proposal for a native test that would make the [[FunctionKind]] slot observable insofar as revealing whether a function was created with class:

https://github.com/caitp/TC39-Proposals/blob/master/tc39-reflect-isconstructor-iscallable.md

If this proposal or something like it advances, we would gain a way to solve this problem concretely when it comes to class at least.

* Ignoring the Annex B [[IsHTMLDDA]] case.

Solution 4

Ran some performance benchmarks on the different approaches mentioned in this thread, here is an overview:


Native Class - Props Method (fastest by 56x on large examples, and 15x on trivial examples):

function isNativeClass (thing) {
    return typeof thing === 'function' && thing.hasOwnProperty('prototype') && !thing.hasOwnProperty('arguments')
}

Which works because the following is true:

> Object.getOwnPropertyNames(class A {})
[ 'length', 'name', 'prototype' ]
> Object.getOwnPropertyNames(class A { constructor (a,b) {} })
[ 'length', 'name', 'prototype' ]
> Object.getOwnPropertyNames(class A { constructor (a,b) {} a (b,c) {} })
[ 'length', 'name', 'prototype' ]
> Object.getOwnPropertyNames(function () {})
[ 'length', 'name', 'arguments', 'caller', 'prototype' ]
> Object.getOwnPropertyNames(() => {})
> [ 'length', 'name' ]

Native Class - String Method (faster than regex method by about 10%):

/**
 * Is ES6+ class
 * @param {any} value
 * @returns {boolean}
 */
function isNativeClass (value /* :mixed */ ) /* :boolean */ {
    return typeof value === 'function' && value.toString().indexOf('class') === 0
}

This may also be of use for determining a Conventional Class:

// Character positions
const INDEX_OF_FUNCTION_NAME = 9  // "function X", X is at index 9
const FIRST_UPPERCASE_INDEX_IN_ASCII = 65  // A is at index 65 in ASCII
const LAST_UPPERCASE_INDEX_IN_ASCII = 90   // Z is at index 90 in ASCII

/**
 * Is Conventional Class
 * Looks for function with capital first letter MyClass
 * First letter is the 9th character
 * If changed, isClass must also be updated
 * @param {any} value
 * @returns {boolean}
 */
function isConventionalClass (value /* :any */ ) /* :boolean */ {
    if ( typeof value !== 'function' )  return false
    const c = value.toString().charCodeAt(INDEX_OF_FUNCTION_NAME)
    return c >= FIRST_UPPERCASE_INDEX_IN_ASCII && c <= LAST_UPPERCASE_INDEX_IN_ASCII
}

I'd also recommend checking out my typechecker package which includes the use cases for the above - via the isNativeClass method, isConventionalClass method, and a isClass method that checks for both types.

Solution 5

Looking at the compiled code generated by Babel, I think there is no way you can tell if a function is used as a class. Back in the time, JavaScript didn't have classes, and every constructor was just a function. Today's JavaScript class keyword don't introduce a new concept of 'classes', it's rather a syntax sugar.

ES6 code:

// ES6
class A{}

ES5 generated by Babel:

// ES5
"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var A = function A() {
    _classCallCheck(this, A);
};

Of course, if you are into coding conventions, you could parse the function (the class), and check if it's name starts with a capital letter.

function isClass(fn) {
    return typeof fn === 'function' && /^(?:class\s+|function\s+(?:_class|_default|[A-Z]))/.test(fn);
}

EDIT:

Browsers which already support the class keyword can use it when parsing. Otherwise, you are stuck with the capital letter one.

EDIT:

As balupton pointed out, Babel generates function _class() {} for anonymous classes. Improved regex based on that.

EDIT:

Added _default to the regex, to detect classes like export default class {}

Warning

BabelJS is heavily under development, and there is no guarantee they will not change the default function names in these cases. Really, you shouldn't rely on that.

Share:
10,978
Moncader
Author by

Moncader

Updated on June 06, 2022

Comments

  • Moncader
    Moncader almost 2 years

    In ECMAScript 6 the typeof of classes is, according to the specification, 'function'.

    However also according to the specification you are not allowed to call the object created via the class syntax as a normal function call. In other words, you must use the new keyword otherwise a TypeError is thrown.

    TypeError: Classes can’t be function-called

    So without using try catch, which would be very ugly and destroy performance, how can you check to see if a function came from the class syntax or from the function syntax?

  • Moncader
    Moncader about 9 years
    Yes, one of the reasons is probably this. However, how can you check if it's a class or function like the question asks?
  • webduvet
    webduvet about 9 years
    As I said using class does the above sequence of steps. so there is no such a thing as typeof 'class'. it is type of function because it is function. Checking on runtime whether to use 'new' or nor is really really really bad practice. So bad that ES6 creators decided to throw syntax error in that case.
  • Moncader
    Moncader about 9 years
    The ES6 creators decided to throw a syntax error if you attempt to call the function, not check it. Say you are building a library that will receive function objects and decide to execute them or store them in some class database (just a random idea). You have no way to know what kind of object you have received right now it seems. So without executing, how can you check if it can be executed or not?
  • Marco Bonelli
    Marco Bonelli about 9 years
    Interesting solution, but this won't work in all the cases, so this is not a real solution.
  • Miguel Mota
    Miguel Mota about 9 years
    Yes I mentioned that, but it's a step closer.
  • webduvet
    webduvet about 9 years
    any function can be called as constructor, that is just how javascript works. so if you are building a class library any function is just as good in it as ES6 'class' .
  • webduvet
    webduvet about 9 years
    you are trying to solve a language feature here guys... there are no classes in JS even if they introduced keyword class.
  • Moncader
    Moncader about 9 years
    @webduvet I disagree. While the classes introduced in ES6 are certainly not on the level of other languages, and they do in fact just reuse already in-existent features of ECMAScript, the semantics of how things created via the class syntax are different than other methods. Therefore it is a distinct feature that cant be called 'JS Classes'.
  • alexpods
    alexpods about 9 years
    @Moogs ofcourse it doesn't work in jsfiddle. It use some compiler like babel, witch compile ES6 class to function.
  • Hacketo
    Hacketo about 9 years
    @Moogs, are you sure that es6fiddle is not converting your javascript in es5 ?
  • alexpods
    alexpods about 9 years
    @Moogs you can check that it works in io.js with harmony flag or in Chrome Canary
  • Hacketo
    Hacketo about 9 years
    @Moogs, you can see the converted es5 code in the sources, file is named _1.js for me
  • Hacketo
    Hacketo about 9 years
    @alexpods, I guess you could just do typeof func === "function" && /^class\s/.test(func+"")
  • alexpods
    alexpods about 9 years
    @Hacketo Yeah, thanks! Why I didn't think about it??? For functions typeof check is good. It's not so good for another types though.
  • Moncader
    Moncader about 9 years
    Yeah. This method should work with the newest spec: people.mozilla.org/~jorendorff/… Since it's supposed to show a representation of the ClassExpression itself just like Function and it must then start with 'class'.
  • balupton
    balupton over 8 years
    Does this still work with anonymous classes compiled to Babel? So anonymous functions get compiled to function _class () {} - source code function a ( ) { return class {} && class {} }, the regex could be modified accordingly.
  • balupton
    balupton over 8 years
    Updated regex for babel anonymous classes: return typeof fn === 'function' && /^(?:class|function (?:[A-Z]|_class))/.test(fn)
  • balupton
    balupton over 8 years
    Hrmm, anonymous classes are different with export default class {}, perhaps just checking for the _classCallCheck is a better option.
  • Tamas Hegedus
    Tamas Hegedus over 8 years
    I think checking for _classCallCheck in the entire function is overkill, and has marginal effect. Checking for the capital starting of names is the weak link here
  • Ethan
    Ethan over 8 years
    @Moogs I think your citation is wrong. F.prototype is set when the class body is evaluated, which is described here: people.mozilla.org/~jorendorff/…; the property descriptor for Object.prototype is something completely different (though it sometimes effects the algorithm linked above). That algorithm is too complicated for me to quickly see whether you're right about F.prototype always being non-enumerable, non-writeable, etc. But if someone wants to look into it more, that's where they should check.
  • Pierre Arnaud
    Pierre Arnaud over 8 years
    @Moogs I've found a case with Babel 6 where the descriptor of prototype has writable: true. Hence I edited your answer and removed the always part of it in your first paragraph. This check does not work for me.
  • Pierre Arnaud
    Pierre Arnaud over 8 years
    You are right. And you cannot add caller or arguments properties on the class after the fact, since both are restricted function properties and trying to add them throws a TypeError.
  • Pierre Arnaud
    Pierre Arnaud over 8 years
    With my setup and Babel 6, functions and classes both return ['length', 'name', 'prototype']. So this does not work for me.
  • mgol
    mgol over 8 years
    It seems it's a spec violation that it doesn't work in Edge, though, I reported it to their bug tracker: connect.microsoft.com/IE/feedback/details/2211653
  • HandyManDan
    HandyManDan about 7 years
    Since toString() return the entire class declaration, that regex may match if the class contains the word class anywhere in the body, e.g. in a string. Safer would be to look for class as the first word class\s{1}
  • Dave W.
    Dave W. about 7 years
    This will return false positives for arrow functions, which also don't have an arguments or caller property. I.e. (() => {}).hasOwnProperty('arguments') returns true.
  • balupton
    balupton about 7 years
    @PierreArnaud that is to be expected, as it is no longer a native class when compiled - so a native class check is not what you want - instead you could use isClass from the typechecker package which checks for both native and conventional classes.
  • Stijn de Witt
    Stijn de Witt about 7 years
    @PierreArnaud But the check for the toString representation starting with 'class' also does not work for transpiled code.
  • loganfsmyth
    loganfsmyth about 6 years
    FYI class\s can fail for minified classes like var foo=class{}; since there is no whitespace.
  • Bergi
    Bergi almost 6 years
    +1 for clever use of proxies :-)
  • Artyer
    Artyer over 5 years
    Fails for class{}, and ({class (){}}.class) (A method called "class"). I think /^class(\s+[^(]|{)/ would work.
  • 0xC0DED00D
    0xC0DED00D about 5 years
    @trincot Fixed it! I was trying to provide an approach, haven't thought about those scenarios.
  • Leo
    Leo about 5 years
    This doesn't work if someone modified Function.prototype.toString. So to ensure it's reliable, check whether Function.prototype.toString.toString() returns "function toString() { [native code] }"
  • Chris Morgan
    Chris Morgan almost 5 years
    Fun thing that may surprise you: isConstructable({a(){}}.a) is false: object shorthand is not purely syntax sugar, because methods created with it are not constructors.
  • Semicolon
    Semicolon almost 5 years
    @ChrisMorgan yes, that’s mentioned in this answer: ‘These functions are not given a [[construct]] slot (I think not many realize that "shorthand method" syntax is does something — it’s not just sugar).’
  • Chris Morgan
    Chris Morgan almost 5 years
    Oops, missed that, sorry!
  • Mike 'Pomax' Kamermans
    Mike 'Pomax' Kamermans over 2 years
    Note that ES6 includes String.prototype.startsWith() so instead of a long regexp and .call, we can just use func.toString().startsWith(`class `)) (with a space following class)