【问题标题】:How to check if a Javascript function is a constructor如何检查 Javascript 函数是否是构造函数
【发布时间】:2016-12-01 23:56:09
【问题描述】:

我注意到并非所有的 Javascript 函数都是构造函数。

var obj = Function.prototype;
console.log(typeof obj === 'function'); //true
obj(); //OK
new obj(); //TypeError: obj is not a constructor

问题一:如何检查函数是否为构造函数,以便可以用new关键字调用?

问题 2:当我创建一个函数时,是否可以将其 NOT 设为构造函数?

【问题讨论】:

  • 有趣的是 FunctionFunction.prototype 仅有的不是构造函数的 function
  • 函数是构造函数。新功能();工作。
  • 检查类型是否为函数即可。
  • Symbol 也不是函数构造函数。 console.log(新符号); //TypeError: Symbol 不是构造函数

标签: javascript


【解决方案1】:

一点背景:

ECMAScript 6+ 区分 callable(可以不用new 调用)和constructable(可以用new 调用)函数:

  • 通过箭头函数语法或通过类或对象字面量中的方法定义创建的函数不可构造
  • 通过class 语法创建的函数不可调用
  • 以任何其他方式(函数表达式/声明、Function 构造函数)创建的函数都是可调用和可构造的。
  • 除非另有明确说明,否则内置函数不可构造。

关于Function.prototype

Function.prototype 是所谓的built-in function that is not constructable。来自规范:

未标识为构造函数的内置函数对象不实现[[Construct]] 内部方法,除非在特定函数的描述中另有说明。

Function.prototype 的值是在运行时初始化的最开始创建的。它基本上是一个空函数,并没有明确说明它是可构造的。


我如何检查一个函数是否是一个构造函数,以便可以用一个新的来调用它?

没有内置的方法可以做到这一点。你可以trynew调用函数,然后检查错误或者返回true

function isConstructor(f) {
  try {
    new f();
  } catch (err) {
    // verify err is the expected error and then
    return false;
  }
  return true;
}

但是,这种方法不是故障安全的,因为函数可能会产生副作用,所以在调用 f 之后,您不知道环境处于哪种状态。

另外,这只会告诉你函数是否可以作为构造函数调用,而不是打算作为构造函数调用。为此,您必须查看文档或函数的实现。

注意:绝不应该有理由在生产环境中使用这样的测试。是否应该使用 new 调用函数应该可以从其文档中看出。

当我创建一个函数时,如何使它不是构造函数?

要创建一个函数真的不是可构造的,你可以使用箭头函数:

var f = () => console.log('no constructable');

根据定义,箭头函数是不可构造的。或者,您可以将函数定义为对象或类的方法。

否则,您可以通过检查函数的 this 值来检查函数是否被 new(或类似的东西)调用,如果是则抛出错误:

function foo() {
  if (this instanceof foo) {
    throw new Error("Don't call 'foo' with new");
  }
}

当然,由于还有其他方法可以设置this的值,所以可能会出现误报。


示例

function isConstructor(f) {
  try {
    new f();
  } catch (err) {
    if (err.message.indexOf('is not a constructor') >= 0) {
      return false;
    }
  }
  return true;
}

function test(f, name) {
  console.log(`${name} is constructable: ${isConstructor(f)}`);
}

function foo(){}
test(foo, 'function declaration');
test(function(){}, 'function expression');
test(()=>{}, 'arrow function');

class Foo {}
test(Foo, 'class declaration');
test(class {}, 'class expression');

test({foo(){}}.foo, 'object method');

class Foo2 {
  static bar() {}
  bar() {}
}
test(Foo2.bar, 'static class method');
test(new Foo2().bar, 'class method');

test(new Function(), 'new Function()');

【讨论】:

  • 如果构造函数有参数isConstructor返回false。
  • '应该从它的文档中看出一个函数是否应该用 new 调用。'- 一点也不。 JS 是一种鸭式语言。在某些情况下,需要对参数进行某种运行时“类型”检查,函数是否可构造就是其中之一。
  • const isConstructor = fn => typeof fn === 'function' && 'prototype' in fn 怎么样?我可以看到箭头函数没有原型
  • @Frondor:这对于 99% 的情况可能已经足够了。我还需要补充一点,当然可以在箭头函数上手动定义 prototype 属性。另请参阅此评论:stackoverflow.com/questions/40922531/…
【解决方案2】:

您正在寻找函数是否具有[[Construct]] 内部方法。内部方法IsConstructor详细步骤:

IsConstructor(argument)

ReturnIfAbrupt(argument).  // (Check if an exception has been thrown; Not important.)  
If Type(argument) is not Object, return false.  // argument === Object(argument), or (typeof argument === 'Object' || typeof argument === 'function')  
If argument has a [[Construct]] internal method, return true.  
Return false.

现在我们需要找到使用了IsConstructor,但没有调用[[Construct]](通常通过Construct内部方法)的地方。

发现用在String函数的newTarget(js中为new.target),可以和Reflect.construct一起使用:

function is_constructor(f) {
  try {
    Reflect.construct(String, [], f);
  } catch (e) {
    return false;
  }
  return true;
}

(我本来可以用任何东西,比如Reflect.construct(Array, [], f);,但String是第一个)

这会产生以下结果:

// true
is_constructor(function(){});
is_constructor(class A {});
is_constructor(Array);
is_constructor(Function);
is_constructor(new Function);

// false
is_constructor();
is_constructor(undefined);
is_constructor(null);
is_constructor(1);
is_constructor(new Number(1));
is_constructor(Array.prototype);
is_constructor(Function.prototype);
is_constructor(() => {})
is_constructor({method() {}}.method)

我发现它不起作用的唯一值是Symbol,尽管new Symbol 在Firefox 中抛出TypeError: Symbol is not a constructoris_constructor(Symbol) === true。这是技术上正确的答案,因为Symbol 确实有一个[[Construct]]内部方法(这意味着它也可以被子类化),但使用new或@ 987654348@ 是Symbol 抛出错误的特殊情况(所以,Symbol 是一个构造函数,错误信息是错误的,它不能作为一个。)你可以在顶部添加if (f === Symbol) return false;不过。

类似这样的事情也一样:

function not_a_constructor() {
  if (new.target) throw new TypeError('not_a_constructor is not a constructor.');
  return stuff(arguments);
}

is_constructor(not_a_constructor);  // true
new not_a_constructor;  // TypeError: not_a_constructor is not a constructor.

因此,作为构造函数的功能的意图不能像这样被理解(直到添加像 Symbol.is_constructor 或其他标志之类的东西)。

注>

【讨论】:

  • 这真是天才!
  • 这是一个聪明的解决方案,但除了Function.prototype,您可以通过检查typeof f === "function" 是否得到相同的结果。
  • @JeffRose 抱歉,我似乎没有添加足够的负面例子。特别是箭头功能。 is_constructor(() => {}) 是假的。
  • 这仅适用于我相信没有参数的构造函数。 is_constructor(URL);例如会失败
【解决方案3】:

有一种快速简便的方法可以确定函数是否可以被实例化,而无需使用 try-catch 语句(v8 无法优化)

function isConstructor(obj) {
  return !!obj.prototype && !!obj.prototype.constructor.name;
}
  1. 首先我们检查对象是否是原型链的一部分。
  2. 然后我们排除匿名函数

有一个警告,即:functions named inside a definition 仍会产生一个名称属性并因此通过此检查,因此在依赖函数构造函数的测试时需要小心。

在以下示例中,该函数不是匿名的,而是实际上称为“myFunc”。它的原型可以扩展为任何 JS 类。

let myFunc = function () {};

【讨论】:

  • 为什么要排除匿名函数?它们是可构造的。
  • 我更喜欢 Object.hasOwnProperty("prototype") 这个功能。顺便说一句,虽然大多数可构造物确实有.prototype,但有些像绑定函数没有,但它们仍然是可构造物。这是因为可构造性与.prototype 没有直接关系,这都与内部[[construct]] 方法有关。 因此,虽然这在许多情况下是一个不错的解决方案,但它并非完全无懈可击(正如您所指出的)。
  • isConstructor(class {}) 返回false
  • @doubleOrt 除此之外,拥有prototype 属性并不意味着函数是构造函数。例如,内置 Symbol 有一个 prototype 属性。但是,new Symbol() 会抛出 not a constructor TypeError。
  • ECMAScript 规范指定规则和实现步骤是不是有点傻(比如测试[[construct]],在常规 ECMAScript 中没有明确的实现?不知何故,这似乎有点坏了.我们显然可以做的最好的事情是在大多数情况下近似它。我正在尝试为提议的新方法实现一个polyfill并尝试遵循提议的规范,但这显然不是完全可能的,因为没有办法测试某物是否真的是构造函数。
【解决方案4】:

使用 ES6+ 代理,无需实际调用构造函数即可测试 [[Construct]]。这是一个sn-p:

const handler={construct(){return handler}} //Must return ANY object, so reuse one
const isConstructor=x=>{
    try{
        return !!(new (new Proxy(x,handler))())
    }catch(e){
        return false
    }
}

如果传递的项不是对象,Proxy 构造函数会抛出错误。如果它不是一个可构造的对象,那么new 会抛出一个错误。但是如果它是一个可构造的对象,那么它会返回 handler 对象而不调用它的构造函数,然后它不会被添加到 true 中。

如您所料,Symbol 仍被视为构造函数。那是因为它是,并且当调用[[Construct]] 时,实现只是抛出一个错误。当new.target 存在时抛出错误的任何用户定义函数都可能出现这种情况,因此将其作为附加检查特别清除似乎是不正确的,但如果您发现它是,请随意这样做有帮助。

【讨论】:

    【解决方案5】:

    如果函数是构造函数,那么它将有一个“原型”成员,而该成员又具有一个与函数本身相同的“构造函数”成员。

    function isConstructor(func) {
        return typeof func === 'function' && !!func.prototype && func.prototype.constructor === func;
    }
    

    【讨论】:

    • 请修正括号。试试isConstructor(false)。 SCNR :-)
    • 引用@doubleOrt:“虽然大多数可构造函数都有.prototype,但有些像绑定函数没有,但它们仍然是可构造函数。这是因为可构造函数没有与.prototype"有直接关系
    • 上面被命名为isConstructor 不是isConstructable,我的意思是,这将返回falseBound-constructors 没关系,因为“绑定”意味着重定向到某个东西,而不是被直接一个真正的构造函数。
    【解决方案6】:

    有一种快速简便的方法可以确定函数是否可以被实例化,而无需使用 try-catch 语句(v8 无法优化)

    function isConstructor(value) {
        return !!value && !!value.prototype && !!value.prototype.constructor;
    }
    
    1. 首先,我们检查value 是否真实。
    2. 然后检查value 是否是原型链的一部分。
    3. 最后,只需检查是否设置了constructor ;-)

    注意上面命名为isConstructor不是isConstructable,我的意思是,这将返回false for Bound-constructors,如cmets中所说,因为“绑定”意味着重定向到一些东西,而不是直接成为真正的构造函数。

    所以这回答了标题的“check ... is constructor”问题,而不是后来的“check ... can be called with new”问题。

    例子:

    const myClazz = {method() {}};
    myClazz.method = myClazz.method.bind(myClazz);
    
    // We can call above with new keyword.
    new (myClazz.method);
    
    // But it's just a callback.
    if (isConstructor(myClass))
       throw new Error('expected to return false for arrow-functions and similar.');
    

    单元测试

    以下内容基于Jasmine

    // Change import to wherever your common functions are 
    // (for me they're in src directory, outside of tests directory).
    import * as common from '../common-tools';
    
    let isMyClassCalled = false;
    class MyClass {
      constructor() {
          isMyClassCalled = true;
      }
    }
    
    describe('App isConstructor tool', () => {
        it('should detect constructor', function () {
            detect(class A {});
            detect(Array);
            detect(Function);
            detect(new Function);
            detect({method() {}}.method);
        });
    
        it('should NOT detect as constructor', function () {
            noDetect();
            noDetect(undefined);
            noDetect(null);
            noDetect(1);
            noDetect(new Number(1));
            noDetect(new (function(){}));
            noDetect(Array.prototype);
            noDetect(Function.prototype);
            // Commented because optimizations convert below into function.
            //noDetect((() => {}));
        });
    
        it('should NOT detect bound constructors', function () {
            const clazz = {method() {}};
            clazz.method = clazz.method.bind(clazz);
            noDetect(clazz.method);
        });
    
        it('should never call constructor', function () {
            common.isConstructor(MyClass);
            expect(isMyClassCalled).toBe(false);
        });
    
        function detect(value, expecting = true) {
            expect(common.isConstructor(value))
                .withContext('For "' + value + '" value')
                .toBe(expecting);
        }
    
        function noDetect(value) {
            detect(value, false);
        }
    });
    

    另类

    以上所有测试也通过以下测试。

    function isConstructor(value) {
        return typeof value === 'function' && !!value.prototype && value.prototype.constructor === value;
    }
    

    【讨论】:

    • 我相信detect((() => {})) 应该在另一个测试用例中并且是noDetect((() => {}))。毕竟,箭头函数不是构造函数。 FWIW、stackoverflow.com/a/57862312/218196stackoverflow.com/a/53112237/218196 已经提到过这种方法。
    • @FelixKling 我刚刚检查过,我的 WebPack 正在将其优化为“detect(function () {});”,现在这更加证实了我的答案(在我修复测试之后),因为箭头功能是自动的“绑定" ;-)
    【解决方案7】:

    对于第一个问题,这个助手呢?

    Function.isConstructor = ({ prototype }) => Boolean(prototype) && Boolean(prototype.constructor)
    
    Function.isConstructor(class {}); // true
    Function.isConstructor(function() {}); // true
    Function.isConstructor(() => {}); // false
    Function.isConstructor("a string"); // false
    

    对于问题2,箭头函数是解决方案。它不能用作构造函数,因为它不依赖于与常规函数相同的作用域并且没有原型(实例的定义,类似于真实 OOP 的类定义)

    const constructable = function() { console.log(this); };
    const callable = () => { console.log(this); };
    
    constructable(); // Window {}
    callable(); // Window {}
    new constructable(); // aConstructableFunction {}
    new callable(); // Uncaught TypeError: callable is not a constructor
    

    【讨论】:

      【解决方案8】:

      我尝试了很多解决方法,但都不能满足我的需求,因此我使用反射元数据制作了自己的解决方法。

      目标:检查当前函数是否有它自己的 __class__ 元数据,这些元数据表示此函数是否为构造函数。

      • npm install reflect-metadata
      • 创建类装饰器工厂@Class()
      • 创建一个辅助函数来检查 __class__ 元数据是否附加到当前函数。

      注意:此解决方法中区分构造函数和普通函数或类的唯一方法是使用类装饰器@Class()

      import 'reflect-metadata';
      
      type Constructor<T = any> = new (...args: any[]) => T;
      
      function Class() {
          return function (target: Constructor) {
              if (!!Reflect.getOwnMetadata('__class__', target)) {
                  throw new Error(`Cannot apply @Class decorator on ${target.name} multiple times.`);
              }
              Reflect.defineMetadata('__class__', target, target);
          };
      }
      
      function isConstructor<T>(type: Constructor<T>): boolean {
          if (typeof type !== 'function') return false;
          return !!Reflect.getOwnMetadata('__class__', type);
      }
      
      /*
       * ------------------
       * Example
       * ------------------
       */
      
      @Class()
      class ServiceClass1 {}
      
      class ServiceClass2 {}
      
      function Test() {}
      
      console.log(isConstructor(ServiceClass1)) // true
      console.log(isConstructor(ServiceClass2)) // false
      console.log(isConstructor(Test)) // false
      

      【讨论】:

        【解决方案9】:

        作为Felix Kling's answer 的补充,即使函数不可构造,如果它具有prototype 属性,我们仍然可以像构造函数一样使用它。我们可以在Object.create() 的帮助下做到这一点。示例:

        // The built-in object Symbol is not constructable, even though it has a "prototype" property:
        new Symbol
        // TypeError: Symbol is not a constructor.
        Object.create(Symbol.prototype);
        // Symbol {}
        //   description: (...)
        //   __proto__: Symbol
        

        【讨论】:

          猜你喜欢
          • 2017-01-13
          • 1970-01-01
          • 2010-09-26
          • 2013-01-11
          • 2012-09-19
          • 2011-06-08
          • 2015-03-06
          • 2012-08-28
          • 2022-11-02
          相关资源
          最近更新 更多