你如何检查ECMAScript 6类和函数之间的区别?

Mon*_*der 33 javascript ecmascript-6 es6-class

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

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

TypeError: Classes can’t be function-called

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

ale*_*ods 31

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

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

所以检查功能看起来很简单:

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


Mig*_*ota 11

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

这是一种检查方式:

class F { }

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

默认情况下,常规函数是可写的,不可枚举的,不可配置的.

function G() { }

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

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

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

// Throws Error
Object.defineProperty(F, 'prototype', {
  writable: true
});
Run Code Online (Sandbox Code Playgroud)

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

// Works
Object.defineProperty(G, 'prototype', {
  writable: false
});
Run Code Online (Sandbox Code Playgroud)

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

@alexpods的字符串化函数和检查class关键字的方法可能是目前最好的解决方案.

  • @webduvet我不同意.虽然ES6中引入的类肯定不在其他语言的层面上,并且它们实际上只是重用已经存在的ECMAScript特性,但是通过类语法创建的事物的语义与其他方法不同.因此,它是一个独特的功能,不能被称为'JS类'. (2认同)

bal*_*ton 8

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


本机类 - 道具方法(在大型示例上最快56x,在普通示例上最快15x):

function isNativeClass (thing) {
    return typeof thing === 'function' && thing.hasOwnProperty('prototype') && !thing.hasOwnProperty('arguments')
}
Run Code Online (Sandbox Code Playgroud)

哪个有效,因为以下是真的:

> 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' ]
Run Code Online (Sandbox Code Playgroud)

Native Class - String方法(比正则表达式方法快10%左右):

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

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

// 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
}
Run Code Online (Sandbox Code Playgroud)

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

  • @PierreArnaud 这是意料之中的,因为它在编译时不再是本机类-因此本机类检查不是您想要的-相反,您可以使用 [`typechecker` 包](https:// github.com/bevry/typechecker)检查本机和传统类。 (2认同)

Sem*_*lon 7

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

我特别要指出的是,可以肯定地回答“是否可以构建此值”这个问题。诚然,通常它本身并没有用。如果您需要知道是否可以调用一个值,那么同样的基本问题仍然存在。

有什么可建设性的吗?

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

  1. 从字面上看,该值是否具有[[construct]]插槽?如果是这样,它是可构造的。如果不是,则无法构造。
  2. 是否打算构造此功能?我们可以产生一些负面影响:不能构造的功能不是要构造的。但是我们也不能说(不求助于启发式检查)是否可构造的函数不是要用作构造函数。

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

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

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

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
Run Code Online (Sandbox Code Playgroud)

请注意,箭头函数,异步函数,生成器和方法不是“传统”函数声明和表达式的双重职责。没有为这些函数提供[[construct]]插槽(我想没有很多人意识到“速记方法”语法在起作用-不仅仅是糖)。

综上所述,如果您的问题确实是“可以构建的”,则上述结论是确定的。不幸的是没有别的。

可以打电话吗?

我们将不得不再次澄清这个问题,因为如果我们非常直白的话,以下测试实际上是有效的*:

const isCallable = fn => typeof fn === 'function';
Run Code Online (Sandbox Code Playgroud)

这是因为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!
Run Code Online (Sandbox Code Playgroud)

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

启发式选项

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

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

正如balupton指出的那样,“调用者”属性的属性描述符的存在与否可以指示如何创建函数。绑定的函数外来对象即使包装了该函数,也不会具有此自有属性。该属性将通过继承自来存在 Function.prototype,但是对于类构造函数也是如此。

同样,即使绑定的函数是使用class创建的,BFEO的toString通常也将开始“函数”。现在,一种检测BFEO本身的方法就是查看它们的名称是否以“ bound”开头,但是不幸的是,这是一个死胡同。它仍然没有告诉我们什么是必然-这是不透明给我们。

但是,如果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
Run Code Online (Sandbox Code Playgroud)

带有箭头的情况指出了我们在哪里获得不特别喜欢的答案。

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


以上据我所知,描述了当代ES的可能和不可能。它并不能满足特定于ES5和更早版本的需求,尽管实际上在ES5和更早版本中,对于任何功能,两个问题的答案始终都是“ true”。

未来

有人建议进行本机测试,以使[[FunctionKind]]插槽在显示是否使用以下函数创建的范围内是可观察的class

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

如果这项提议或类似的提议取得进展,class至少在涉及方面,我们将获得一种具体解决此问题的方法。

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