41

typeof根据规范,在 ECMAScript 6 中,类是'function'.

但是,根据规范,您也不允许将通过类语法创建的对象作为普通函数调用来调用。换句话说,您必须使用new关键字,否则会引发 TypeError。

TypeError: Classes can’t be function-called

因此,如果不使用会非常丑陋并破坏性能的 try catch,您如何检查函数是来自class语法还是来自function语法?

4

9 回答 9

42

我认为检查函数是否为 ES6 类的最简单方法是检查 .toString()方法的结果。根据es2015 规范

字符串表示必须具有 FunctionDeclaration FunctionExpression、GeneratorDeclaration、GeneratorExpression、ClassDeclarationClassExpression、ArrowFunction、MethodDefinition 或 GeneratorMethod 的语法,具体取决于对象的实际特征

所以检查函数看起来很简单:

function isClass(func) {
  return typeof func === 'function' 
    && /^class\s/.test(Function.prototype.toString.call(func));
}
于 2015-03-17T08:19:09.433 回答
16

我做了一些研究,发现 ES6 类的原型对象 [ spec 19.1.2.16 ] 似乎是不可写的、不可枚举的不可配置的

这是一种检查方法:

class F { }

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

默认情况下,常规函数是writeablenon-enumerablenon-configurable

function G() { }

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

ES6 小提琴:http ://www.es6fiddle.net/i7d0eyih/

因此,ES6 类描述符将始终将这些属性设置为 false,如果您尝试定义描述符,则会引发错误。

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

但是,使用常规函数,您仍然可以定义这些描述符。

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

在常规函数上修改描述符并不常见,因此您可以使用它来检查它是否是一个类,但这当然不是一个真正的解决方案。

@alexpods 对函数进行字符串化并检查 class 关键字的方法可能是目前最好的解决方案。

于 2015-03-17T08:06:27.303 回答
15

由于现有答案从 ES5 环境的角度解决了这个问题,我认为从 ES2015+ 的角度提供答案可能是值得的;最初的问题没有具体说明,今天很多人不再需要转译类,这稍微改变了情况。

特别是我想指出,可能明确回答“可以构造这个值吗?”这个问题。诚然,这本身通常没有用。如果您需要知道是否可以调用某个值,那么同样的基本问题仍然存在。

有什么可建造的吗?

首先,我认为我们需要澄清一些术语,因为询问一个值是否是构造函数可能意味着不止一件事:

  1. 从字面上看,这个值是否有一个 [[construct]] 槽?如果是这样,它是可构造的。如果没有,则它是不可构造的。
  2. 这个函数是打算构建的吗?我们可以得出一些否定的结论:无法构造的函数并不是要构造的。但是我们也不能说(不求助于启发式检查)一个可构造的函数是否打算用作构造函数。

使 2 无法回答的是,function仅使用关键字创建的函数既可构造又可调用,但此类函数通常仅用于其中一个目的。正如其他一些人所提到的,2 也是一个可疑的问题——它类似于问“作者在写这篇文章时在想什么?” 我认为 AI 还不存在 :) 虽然在一个完美的世界中,也许所有作者都会为构造函数保留 PascalCase(请参阅 balupton 的isConventionalClass函数),但实际上,在此测试中遇到误报/误报并不罕见。

关于这个问题的第一个版本,是的,我们可以知道一个函数是否可构造。显而易见的事情是尝试构建它。虽然这不是真的可以接受,因为我们不知道这样做是否会产生副作用 - 似乎我们对函数的性质一无所知,因为如果我们知道,我们就不需要此检查)。幸运的是,有一种方法可以在不真正构造构造函数的情况下构造它:

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

代理处理程序可以覆盖代理值的construct[[construct]],但它不能使不可构造的值可构造。所以我们可以“模拟实例化”输入来测试这是否失败。请注意,构造陷阱必须返回一个对象。

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

请注意,箭头函数、异步函数、生成器和方法不像“传统”函数声明和表达式那样具有双重职责。这些函数没有被赋予 [[construct]] 槽(我认为没有多少人意识到“速记方法”语法是有作用的——它不仅仅是糖)。

因此,回顾一下,如果您的问题真的是“这是否可构造”,那么以上内容就是结论性的。不幸的是,没有别的了。

有什么可调用的吗?

我们必须再次澄清这个问题,因为如果我们非常直白,下面的测试实际上是有效的*:

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

这是因为 ES 目前不允许你创建一个没有 [[call]] 槽的函数(好吧,绑定函数没有直接的,但它们代理到一个有的函数)。

这可能看起来不正确,因为如果您尝试调用它们而不是构造它们,则使用类语法创建的构造函数会抛出异常。然而它们是可调用的——只是它们的 [[call]] 槽被定义为一个抛出的函数!哦。

我们可以通过将我们的第一个函数转换为它的镜像来证明这一点。

// 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!

这样的功能没有帮助,但我想展示这些结果以使问题的本质成为焦点。我们不能轻易检查一个函数是否“仅新”的原因是答案不是根据“不存在调用”来建模的,而“从不新”是根据“不存在构造”来建模的。我们有兴趣知道的东西隐藏在一种我们无法观察到的方法中,除非通过它的评估,所以我们所能做的就是使用启发式检查作为我们真正想知道的东西的代理。

启发式选项

我们可以从缩小模棱两可的情况开始。任何不可构造的函数在两种意义上都是明确可调用的:如果 typeof fn === 'function'但是isConstructable(fn) === false,我们有一个只调用函数,例如箭头、生成器或方法。

所以这四个感兴趣的案例是class {}function() {}加上两者的绑定形式。我们能说的其他一切都是可调用的。请注意,当前的答案都没有提到绑定函数,但是这些都给任何启发式检查带来了重大问题。

正如 balupton 所指出的,“调用者”属性的属性描述符是否存在可以作为函数创建方式的指标。一个绑定的函数外来对象不会有这个自己的属性,即使它包装的函数有。该属性将通过继承自 存在 Function.prototype,但对于类构造函数也是如此。

同样,BFEO 的 toString 通常会以“函数”开头,即使绑定函数是使用类创建的。现在,检测 BFEO 本身的启发式方法是查看它们的名称是否以“绑定”开头,但不幸的是,这是一条死胡同;它仍然没有告诉我们什么是绑定的——这对我们来说是不透明的。

但是,如果 toString 确实返回了“类”(例如,对于 DOM 构造函数,这不是真的),这是一个非常可靠的信号,表明它是不可调用的。

我们能做的最好的事情是这样的:

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

带箭头的案例指出我们在哪里得到我们不特别喜欢的答案。

在 isProbablyNotCallable 函数中,条件的最后一部分可以替换为来自其他答案的其他检查;我在这里选择了 Miguel Mota,因为它恰好也可以与(大多数?)DOM 构造函数一起使用,即使是那些在 ES 类引入之前定义的构造函数。但这并不重要——每一个可能的检查都有一个缺点,而且没有魔法组合。


据我所知,以上描述了当代 ES 中什么是可能的,什么是不可能的。它没有解决特定于 ES5 和更早版本的需求,尽管实际上在 ES5 和更早版本中,对于任何函数,这两个问题的答案总是“正确的”。

未来

有一个原生测试的提议,它可以让 [[FunctionKind]] 槽可观察到,以揭示一个函数是否是用class:

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

如果这个提议或类似的提议取得进展,我们class至少会找到具体解决这个问题的方法。

* 忽略附件 B [[IsHTMLDDA]] 的情况。

于 2018-03-27T10:44:45.840 回答
12

对这个线程中提到的不同方法进行了一些性能基准测试,这里是一个概述:


原生类 - 道具方法(大型示例快 56 倍,小示例快 15 倍):

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

之所以有效,是因为以下情况属实:

> 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' ]

原生类 - 字符串方法(比正则表达式方法快约 10%):

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

这也可用于确定常规类别:

// 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
}

我还建议检查我的typechecker,其中包含上述用例 - 通过isNativeClass方法、isConventionalClass方法和isClass检查这两种类型的方法。

于 2015-08-26T20:00:24.953 回答
3

查看Babel生成的编译代码,我认为您无法判断函数是否用作类。回到过去,JavaScript 没有类,每个构造函数都只是一个函数。今天的 JavaScript 类关键字并没有引入“类”的新概念,而是一种语法糖。

ES6 代码:

// ES6
class A{}

ES5 由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);
};

当然,如果您喜欢编码约定,您可以解析函数(类),并检查它的名称是否以大写字母开头。

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

编辑:

已经支持 class 关键字的浏览器可以在解析时使用它。否则,您将被大写字母 1 卡住。

编辑:

正如 balupton 指出的,Babelfunction _class() {}为匿名类生成。基于此改进的正则表达式。

编辑:

添加_default到正则表达式中,以检测类export default class {}

警告

BabelJS 正在大量开发中,不能保证在这些情况下它们不会更改默认函数名称。真的,你不应该依赖它。

于 2015-08-26T20:16:04.410 回答
1

您可以使用 new.target 来确定它是由 ES6 类函数还是函数构造函数实例化

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

function Person2(){
  this.name='cc'
  console.log(new.target) // => [Function: Person2]
}
于 2019-01-04T18:06:50.117 回答
0

我知道这是一个很老的问题,但我最近一直在想同样的事情,特别是因为如果你尝试将函数用作类组件(例如你设置 babel 将类转换为函数),React 会抛出错误,我有)。我坐了一会儿,并且能够想出这个:

function isClass(f) {
  return typeof f === "function" && (() => { try { f(); return false } catch { return true } })();
}

isClass(function() {}); //false
isClass(() => {}); //false
isClass(class {}); //true

这是基于 ES6 类需要使用new关键字的事实,如果缺少它会抛出错误。该函数首先检查传递的参数的类型是否为函数,这适用于类和函数。然后它尝试在没有new关键字的情况下调用它,这只能由普通函数完成。但是请记住,如果函数通过以下方法检查是否正在使用 new 调用它时抛出错误,这可能会被愚弄if (!(this instanceof ConstructorName)) throw

isClass(function Constructor() {
  if (!(this instanceof Constructor)) throw new Error();
}); // true

function isClass(f) {
  return typeof f === "function" && (() => { try { f(); return false } catch { return true } })();
}

console.log(isClass(function() {})); //false
console.log(isClass(() => {})); //false
console.log(isClass(class {})); //true

console.log(isClass(function Constructor() {
  if (!(this instanceof Constructor)) throw new Error();
})); // true

于 2022-02-16T02:22:11.937 回答
0

虽然没有直接关系,但是如果类、构造函数或函数是由你生成的,并且你想知道是应该调用函数还是使用 new 关键字实例化对象,你可以通过在原型中添加自定义标志来实现构造函数或类。您当然可以使用其他答案(例如toString)中提到的方法从函数中分辨出一个类。但是,如果你的代码是使用 babel 转译的,那肯定是个问题。

为了使其更简单,您可以尝试以下代码 -

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

或者如果使用构造函数 -

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

您可以通过检查原型属性来检查它是否是类。

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

如果类或函数不是由您创建的,则此方法显然不起作用。React.js使用此方法检查React Component是类组件还是函数组件。此答案取自 Dan Abramov 的博客文章

于 2018-12-06T09:51:04.160 回答
-1

如果我正确理解了 ES6,那么使用class的效果与您输入的效果相同

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

键入MyClass()without时的语法错误keyword new只是为了防止对象使用的变量污染全局空间。

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

如果你有

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

但如果你这样做

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

因为this在作为函数调用的构造函数中是指全局对象,但是当使用关键字调用时,newJavascript首先创建新的空对象,然后在其上调用构造函数。

于 2015-03-17T07:44:02.860 回答