根据规范,在ECMAScript 6中,typeof
类是'function'
。
但是,根据规范,您不能将通过类语法创建的对象称为普通函数调用。换句话说,您必须使用new
关键字否则会引发TypeError。
TypeError: Classes can’t be function-called
因此,如果不使用try catch,这将非常难看并破坏性能,您如何检查函数是来自class
语法还是来自function
语法?
我认为检查函数是否为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));
}
我做了一些研究,发现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关键字的方法可能是目前最好的解决方案。
在这个线程中提到的不同方法上运行一些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
方法。
由于现有的答案从ES5环境的角度解决了这个问题,我认为值得从ES2015 +角度提供答案;原来的问题没有具体说明,今天许多人不再需要转移课程,这会改变一些情况。
特别是我想要指出,有可能明确地回答“这个价值能够构建吗?”的问题。不可否认,这本身并不常用;如果你需要知道是否可以调用一个值,那么同样的基本问题仍然存在。
首先,我认为我们需要澄清一些术语,因为询问值是否是构造函数可能意味着不止一件事:
让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!
这样的功能没有帮助,但我想展示这些结果以使问题的性质成为焦点。我们不能轻易检查某个函数是否是“仅新的”的原因是,答案没有根据“没有调用”来建模,“永不新”的方式是根据“缺少构造”来建模的。我们感兴趣的东西埋藏在一种我们无法通过其评估观察到的方法中,所以我们所能做的就是使用启发式检查作为我们真正想知道的代理。
我们可以从缩小模糊的案例开始。任何不可构造的函数都可以在两种意义上明确地调用:如果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]]案例。
看看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正在大力开发中,并且无法保证在这些情况下它们不会更改默认函数名称。真的,你不应该依赖它。
您可以使用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]
}
虽然它没有直接相关,但是如果类,构造函数或函数是由您生成的,并且您想知道是否应该使用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
如果我正确理解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首先创建新的空对象,然后在其上调用构造函数。