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

断了今生、忘了曾经 提交于 2019-12-17 17:37:06

问题


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?


回答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));
}



回答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.




回答3:


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.




回答4:


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.




回答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.




回答6:


You can use new.target to determine whether whether its instantiated by ES6 class function or function constructor

class Person1 {
  constructor(name) {
    this.name = name;
    console.log(new.target) // => // => [Class: Person1]
  }
}

function Person2(){
  this.name='cc'
  console.log(new.target) // => [Function: Person2]
}



回答7:


if I understood ES6 correctly using class has the same effect as if you were typing

var Foo = function(){}
var Bar = function(){
 Foo.call(this);
}
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.constructor = Bar;

syntax error when typing MyClass() without keyword new is just to prevent polluting global space with variables intended to be used by object.

var MyClass = function(){this.$ = "my private dollar"; return this;}

if you have

// $ === jquery
var myObject = new MyClass();
// $ === still jquery
// myObject === global object

but if you do

var myObject = MyClass();
// $ === "My private dollar"

because this in Constructor called as function refers to global object, but when called with keyword new Javascript first creates new empty object and then calls the constructor on it.




回答8:


Although it's not directly related, but if the class, constructor or function is generated by you and you want to know whether you should call the function or instantiate an object using new keyword, you can do that by adding a custom flag in the prototype of the constructor or class. You can certainly tell a class from a function using methods mentioned in other answers (such as toString). However, if your code is transpiled using babel, it would certainly be a problem.

To make it simpler, you can try the following code -

class Foo{
  constructor(){
    this.someProp = 'Value';
  }
}
Foo.prototype.isClass = true;

or if using constructor function -

function Foo(){
  this.someProp = 'Value';
}
Foo.prototype.isClass = true;

and you can check whether it's class or not by checking on the prototype property.

if(Foo.prototype.isClass){
  //It's a class
}

This method obviously won't work if the class or function is not created by you. React.js uses this method to check whether the React Component is a Class Component or a Function Component. This answer is taken from Dan Abramov's blog post



来源:https://stackoverflow.com/questions/29093396/how-do-you-check-the-difference-between-an-ecmascript-6-class-and-function

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!