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

问题描述 投票:31回答:8

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

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

TypeError: Classes can’t be function-called

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

javascript ecmascript-6 es6-class
8个回答
29
投票

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

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

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

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

11
投票

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

这是一种检查方式:

class F { }

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

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

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关键字的方法可能是目前最好的解决方案。


8
投票

在这个线程中提到的不同方法上运行一些performance benchmarks,这里是一个概述:


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

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

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
}

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

// 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 package,其中包括上述用例 - 通过isNativeClass方法,isConventionalClass方法和检查这两种类型的isClass方法。


7
投票

由于现有的答案从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]]槽被定义为抛出的函数! Oy公司。

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

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

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

Heuristic options

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

因此,感兴趣的四个案例是class {}function() {}以及两者的约束形式。我们可以说的其他所有东西都只能赎回。请注意,当前的答案都没有提到绑定函数,但这些问题会给任何启发式检查带来重大问题。

正如balupton指出的那样,'caller'属性的属性描述符的存在与否可以作为函数创建方式的指示。绑定函数奇异对象即使它包装的函数也没有这个自有属性。该属性将通过Function.prototype的继承来存在,但对于类构造函数也是如此。

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

但是,如果toString确实返回'class'(对于例如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]]案例。


2
投票

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

ES6代码:

// ES6
class A{}

Babel生成的ES5:

// 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关键字的浏览器可以在解析时使用它。否则,你会被大写字母卡住。

编辑:

正如balupton指出的那样,巴贝尔为匿名课程制作了function _class() {}。基于此改进的正则表达式。

编辑:

_default添加到正则表达式,以检测像export default class {}这样的类

警告

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


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

1
投票

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

为简化起见,您可以尝试以下代码 -

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

或者如果使用构造函数 -

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

你可以通过检查prototype属性来检查它是否是类。

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

如果您没有创建类或函数,则此方法显然不起作用。 React.js使用此方法检查React Component是类组件还是功能组件。这个答案来自Dan Abramov的blog post


0
投票

如果我正确理解ES6使用class与你输入的效果相同

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

在没有MyClass()的情况下键入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引用了全局对象,但是当使用关键字new调用时,Javascript首先创建新的空对象,然后在其上调用构造函数。

© www.soinside.com 2019 - 2024. All rights reserved.