如何检测函数是否被称为构造函数?

时间:2008-12-15 08:30:10

标签: javascript constructor

给定一个功能:

function x(arg) { return 30; }

您可以通过两种方式调用它:

result = x(4);
result = new x(4);

第一个返回30,第二个返回一个对象。

如何检测函数本身内部函数的哪种方式

无论您的解决方案是什么,它都必须使用以下调用:

var Z = new x(); 
Z.lolol = x; 
Z.lolol();

目前所有解决方案都认为Z.lolol()将其称为构造函数。

22 个答案:

答案 0 :(得分:88)

注意:现在可以在ES2015及更高版本中使用。请参阅Daniel Weiner's answer

我不认为你想要的是什么[在ES2015之前]。功能中没有足够的信息来进行可靠的推理。

查看ECMAScript第3版规范,调用new x()时采取的步骤基本上是:

  • 创建新对象
  • 将其内部[[Prototype]]属性分配给x
  • 的prototype属性
  • 正常调用x,将新对象作为this
  • 传递给它
  • 如果对x的调用返回了一个对象,则将其返回,否则返回新对象

对于如何调用函数没有任何用处可用于执行代码,因此在x内部进行测试的唯一问题是this值,这里的所有答案都是这样做。正如您所观察到的,在调用x作为构造函数时,* x的新实例与调用{x时传递的this的预先存在的实例无法区分{1}}作为一个函数,除非您为x创建的每个新对象分配属性:

x

显然这并不理想,因为你现在在function x(y) { var isConstructor = false; if (this instanceof x // <- You could use arguments.callee instead of x here, // except in in EcmaScript 5 strict mode. && !this.__previouslyConstructedByX) { isConstructor = true; this.__previouslyConstructedByX = true; } alert(isConstructor); } 构造的每个可能被覆盖的对象上都有一个额外无用的属性,但我认为这是你能做到的最好的。

(*)“实例”是一个不准确的术语,但足够接近,比“通过调用x作为构造函数创建的对象更简洁”

答案 1 :(得分:61)

从ECMAScript 6开始,这可以通过new.target实现。如果使用new.target(或new调用Reflect.construct调用函数,则会设置new,否则undefinedfunction Foo() { if (new.target) { console.log('called with new'); } else { console.log('not called with new'); } } new Foo(); // "called with new" Foo(); // "not called with new" Foo.call({}); // "not called with new"

{{1}}

答案 2 :(得分:51)

1)您可以查看this.constructor

function x(y)
{
    if (this.constructor == x)
        alert('called with new');
    else
         alert('called as function');
}

2)是的,在new上下文

中使用时,返回值才会被丢弃

答案 3 :(得分:19)

  

注意:此答案是用 2008 编写的,当javascript仍然来自 1999 ES3 时。从那时起,添加了许多新功能,因此现在存在更好的解决方案。这个答案是出于历史原因而保留的。

以下代码的好处是您不需要指定函数的名称两次,它也适用于匿名函数。

function x() {
    if ( (this instanceof arguments.callee) ) {
      alert("called as constructor");
    } else {
      alert("called as function");
    }
}

<强>更新 正如claudiu在下面的注释中指出的那样,如果将构造函数分配给它创建的同一对象,则上述代码不起作用。我从来没有编写那样做的代码,并且有更新的人看到其他人做那个打火机。

Claudius例子:

var Z = new x();
Z.lolol = x;
Z.lolol();

通过向对象添加属性,可以检测对象是否已初始化。

function x() {
    if ( (this instanceof arguments.callee && !this.hasOwnProperty("__ClaudiusCornerCase")) ) {
        this.__ClaudiusCornerCase=1;
        alert("called as constructor");
    } else {
        alert("called as function");
    }
}

如果删除添加的属性,即使上面的代码也会中断。 然而,您可以使用您喜欢的任何值覆盖它,包括undefined,它仍然有效。 但是如果删除它,它将会中断。

此时ecmascript中没有原生支持来检测函数是否被称为构造函数。这是我到目前为止最接近的事情,除非你删除属性,否则它应该有效。

答案 4 :(得分:8)

两种方式,在引擎盖下基本相同。您可以测试this的范围,或者您可以测试this.constructor是什么。

如果您将方法作为构造函数调用this将是该类的新实例,如果您将方法作为方法调用this将是方法的上下文对象。类似地,如果调用new,则对象的构造函数将是方法本身,否则将是系统Object构造函数。这显然是泥,但这应该有所帮助:

var a = {};

a.foo = function () 
{
  if(this==a) //'a' because the context of foo is the parent 'a'
  {
    //method call
  }
  else
  {
    //constructor call
  }
}

var bar = function () 
{
  if(this==window) //and 'window' is the default context here
  {
    //method call
  }
  else
  {
    //constructor call
  }
}

a.baz = function ()
{
  if(this.constructor==a.baz); //or whatever chain you need to reference this method
  {
    //constructor call
  }
  else
  {
    //method call
  }
}

答案 5 :(得分:5)

检查构造函数中[this]的实例类型是可行的方法。问题是没有任何进一步的麻烦,这种方法容易出错。但是有一个解决方案。

让我们说我们正在处理函数ClassA()。最基本的方法是:

    function ClassA() {
        if (this instanceof arguments.callee) {
            console.log("called as a constructor");
        } else {
            console.log("called as a function");
        }
    }

有几种方法表明上述解决方案无法按预期工作。考虑这两个:

    var instance = new ClassA;
    instance.classAFunction = ClassA;
    instance.classAFunction(); // <-- this will appear as constructor call

    ClassA.apply(instance); //<-- this too

为了克服这些问题,有人建议a)将一些信息放在实例的字段中,例如“ConstructorFinished”并检查它或b)在列表中跟踪构造的对象。我对两者都感到不舒服,因为改变ClassA的每个实例对于类型相关的功能来说太过侵入性和昂贵。如果ClassA将有许多实例,则收集列表中的所有对象可能会提供垃圾收集和资源问题。

要做的就是能够控制ClassA功能的执行。简单的方法是:

    function createConstructor(typeFunction) {
        return typeFunction.bind({});
    }

    var ClassA = createConstructor(
        function ClassA() {
            if (this instanceof arguments.callee) {
                console.log("called as a function");
                return;
            }
            console.log("called as a constructor");
        });

    var instance = new ClassA();

这将有效防止所有尝试使用[this]值进行欺骗。绑定函数将始终保持其原始[this]上下文,除非您使用 new 运算符调用它。

高级版本返回了在任意对象上应用构造函数的功能。一些用法可能是使用构造函数作为类型转换器或在继承方案中提供可调用的基类构造函数链。

    function createConstructor(typeFunction) {
        var result = typeFunction.bind({});
        result.apply = function (ths, args) {
            try {
                typeFunction.inApplyMode = true;
                typeFunction.apply(ths, args);
            } finally {
                delete typeFunction.inApplyMode;
            }
        };
        return result;
    }

    var ClassA = createConstructor(
        function ClassA() {
            if (this instanceof arguments.callee && !arguments.callee.inApplyMode) {
                console.log("called as a constructor");
            } else {
                console.log("called as a function");
            }
        });

答案 6 :(得分:4)

实际上解决方案很可能也很简单...不明白为什么为这么小的东西写了这么多单词

更新: 感谢 TwilightSun 解决方案现已完成,即使是建议测试 Claudiu !谢谢你们!

function Something()
{
    this.constructed;

    if (Something.prototype.isPrototypeOf(this) && !this.constructed)
    {
        console.log("called as a c'tor"); this.constructed = true;
    }
    else
    {
        console.log("called as a function");
    }
}

Something(); //"called as a function"
new Something(); //"called as a c'tor"

在此处演示:https://jsfiddle.net/9cqtppuf/

答案 7 :(得分:3)

没有可靠的方法可以区分JavaScript代码中如何调用函数。 1

但是,函数调用将this分配给全局对象,而构造函数将this分配给新对象。这个新对象永远不可能是全局对象,因为即使实现允许您设置全局对象,您仍然没有机会这样做。

您可以通过将函数调用为函数(heh)来返回this来获取全局对象。

我的直觉是,在ECMAScript 1.3的规范中,当被调用为函数时具有已定义行为的构造函数应该区分使用此比较调用它们的方式:

function MyClass () {
    if ( this === (function () { return this; })() ) {
        // called as a function
    }
    else {
        // called as a constructor
    }
}

无论如何,任何人都可以使用函数或构造函数的callapply并将this设置为任何内容。但是这样,你可以避免“初始化”全局对象:

function MyClass () {
    if ( this === (function () { return this; })() ) {
        // Maybe the caller forgot the "new" keyword
        return new MyClass();
    }
    else {
        // initialize
    }
}

<子> 1。 主机(也称为实现)可能能够区分它是否实现了与内部属性[[Call]][[Construct]]等效的内容。前者是为函数或方法表达式调用的,而后者是为new表达式调用的。

答案 8 :(得分:3)

在我看到这个帖子之前,我从未认为构造函数可能是实例的属性,但我认为以下代码涵盖了这种罕见的情况。

// Store instances in a variable to compare against the current this
// Based on Tim Down's solution where instances are tracked
var Klass = (function () {
    // Store references to each instance in a "class"-level closure
    var instances = [];

    // The actual constructor function
    return function () {
        if (this instanceof Klass && instances.indexOf(this) === -1) {
            instances.push(this);
            console.log("constructor");
        } else {
            console.log("not constructor");
        }
    };
}());

var instance = new Klass();  // "constructor"
instance.klass = Klass;
instance.klass();            // "not constructor"

对于大多数情况,我可能只是检查instanceof。

答案 9 :(得分:3)

扩展Gregs解决方案,这个解决方案与您提供的测试用例完美配合:

function x(y) {
    if( this.constructor == arguments.callee && !this._constructed ) {
        this._constructed = true;
        alert('called with new');
    } else {
        alert('called as function');
    }
}

编辑:添加一些测试用例

x(4);             // OK, function
var X = new x(4); // OK, new

var Z = new x();  // OK, new
Z.lolol = x; 
Z.lolol();        // OK, function

var Y = x;
Y();              // OK, function
var y = new Y();  // OK, new
y.lolol = Y;
y.lolol();        // OK, function

答案 10 :(得分:2)

在我对http://packagesinjavascript.wordpress.com/的测试中,我发现测试if(this == window)在所有情况下都是跨浏览器的,所以这就是我最终使用的那个。

-Stijn

答案 11 :(得分:2)

来自John Resig:

function makecls() {

   return function(args) {

        if( this instanceof arguments.callee) {
            if ( typeof this.init == "function")
                this.init.apply(this, args.callee ? args : arguments)
        }else{
            return new arguments.callee(args);
        }
    };
}

var User = makecls();

User.prototype.init = function(first, last){

    this.name = first + last;
};

var user = User("John", "Resig");

user.name

答案 12 :(得分:2)

如果您要破解,那么instanceofnew.target之后的最小解决方案,而不是其他答案。但是使用instanceof解决方案,这个例子会失败:

let inst = new x;
x.call(inst);

结合@TimDown解决方案,如果您希望与较旧的ECMAScript版本兼容,则可以使用ES6&#39; WeakSet来防止将属性放入实例中。那么,将使用WeakSet以便允许未使用的对象被垃圾收集。 new.target在同一源代码中不会兼容,因为它是ES6的语法功能。 ECMAScript指定标识符不能是保留字之一,new不是对象。

(function factory()
{
    'use strict';
    var log = console.log;

    function x()
    {
        log(isConstructing(this) ?
            'Constructing' :
            'Not constructing'
        );
    }

    var isConstructing, tracks;
    var hasOwnProperty = {}.hasOwnProperty;

    if (typeof WeakMap === 'function')
    {
        tracks = new WeakSet;
        isConstructing = function(inst)
        {
            if (inst instanceof x)
            {
                return tracks.has(inst) ?
                    false : !!tracks.add(inst);
            }
            return false;
        }
    } else {
        isConstructing = function(inst)
        {
            return inst._constructed ?
                false : inst._constructed = true;
        };
    }
    var z = new x; // Constructing
    x.call(z)      // Not constructing
})();

ECMAScript 3的instanceof运算符为specified为:

11.8.6 instanceof运算符
---评估生产RelationalExpression:RelationalExpression instanceof ShiftExpression 如下:
--- 1.评估RelationalExpression --- 2.调用GetValue(结果(1))。
--- 3.评估ShiftExpression --- 4.调用GetValue(结果(3))。
--- 5.如果Result(4)不是对象,则抛出 TypeError 异常 --- 6.如果Result(4)没有[[HasInstance]]方法,则抛出 TypeError 异常。
--- 7.使用参数Result(2)调用Result(4)的[[HasInstance]]方法 --- 8.返回结果(7)。
15.3.5.3 [[HasInstance]](V)
---假设F是一个Function对象 ---当用值V调用F的[[HasInstance]]方法时,采取以下步骤:
--- 1.如果V不是对象,则返回 false --- 2.使用属性名称&#34;原型&#34; 调用F的[[Get]]方法。
--- 3.设O为结果(2)。
--- 4.如果O不是对象,则抛出 TypeError 异常 --- 5.设V为V的[[Prototype]]属性的值。
--- 6.如果V为** null **,则返回 false --- 7.如果O和V引用同一个对象或者它们引用相互连接的对象(13.1.2),则返回 true
--- 8.转到步骤5.

这意味着它会在转到原型后递归左侧值,直到它不是一个对象,或者直到它等于具有指定{{1}的右侧对象的原型。 } 方法。这意味着它会检查左侧是否是右侧的一个实例,尽管消耗了左侧的所有内部原型。

[[HasInstance]]

答案 13 :(得分:1)

也许我错了,但(以寄生虫为代价)以下代码似乎是一个解决方案:

function x(arg) {
    //console.debug('_' in this ? 'function' : 'constructor'); //WRONG!!!
    //
    // RIGHT(as accepted)
    console.debug((this instanceof x && !('_' in this)) ? 'function' : 'constructor');
    this._ = 1;
    return 30;
}
var result1 = x(4),     // function
    result2 = new x(4), // constructor
    Z = new x();        // constructor
Z.lolol = x; 
Z.lolol();              // function

答案 14 :(得分:1)

我相信解决方案是将您的 Constructor 函数转换为真正的 Constructor 函数及其原型 Constructor 的包装器(如果需要)。此方法从 2009 年起适用于 ES5,并且也适用于严格模式。在下面的代码窗口中,我有一个使用模块模式的示例,将真正的构造函数及其原型的构造函数保存在一个闭包中,该闭包可通过 constructor(wrapper) 内的作用域访问。这是有效的,因为没有属性被添加到 Constructor(wrapper) 中的“this”关键字,并且 Constructor(wrapper).prototype 没有设置,Object 默认情况下也是如此;因此,如果 new 关键字已与 Constructor(wrapper) 一起使用,则从 Object.getpropertyNames 返回的数组的长度将等于 0。如果为 true,则返回新向量

var Vector = (function() {
        
     var Vector__proto__ = function Vector() {
         // Vector methods go here
     }
            
     var vector__proto__ = new Vector__proto__();;
        
     var Vector = function(size) {
         // vector properties and values go here
         this.x = 0;
         this.y = 0;
         this.x = 0;
         this.maxLen = size === undefined? -1 : size;
                
     };
     Vector.prototype = vector__proto__;
        
     return function(size){
                
         if ( Object.getOwnPropertyNames(this).length === 0 ) {
             // the new keyword WAS USED with the wrapper constructor
             return new Vector(size); 
         } else { 
             // the new keyword was NOT USED with the wrapper constructor
             return; 
         };
    };
})();

答案 15 :(得分:0)

使用this instanceof arguments.callee(可选地将arguments.callee替换为其所在的函数,从而提高性能)以检查是否将某些内容称为构造函数。 不要使用this.constructor因为可以轻松更改。

答案 16 :(得分:0)

当我尝试实现一个返回字符串而不是对象的函数时,我遇到了同样的问题。

似乎足以在函数的开头检查“this”的存在:

function RGB(red, green, blue) {
    if (this) {
        throw new Error("RGB can't be instantiated");
    }

    var result = "#";
    result += toHex(red);
    result += toHex(green);
    result += toHex(blue);

    function toHex(dec) {
        var result = dec.toString(16);

        if (result.length < 2) {
            result = "0" + result;
        }

        return result;
    }

    return result;
}

无论如何,最后我决定将我的RGB()伪类转换为rgb()函数,所以我不会尝试实例化它,因此根本不需要安全检查。但这取决于你想要做什么。

答案 17 :(得分:0)

在问题的顶部,下面的代码将自动修复该问题,以防在不使用new的情况下调用函数。

function Car() {

    if (!(this instanceof Car)) return new Car();

    this.a = 1;
    console.log("Called as Constructor");

}
let c1 = new Car();
console.log(c1);

答案 18 :(得分:0)

function createConstructor(func) {
    return func.bind(Object.create(null));
}

var myClass = createConstructor(function myClass() {
    if (this instanceof myClass) {
        console.log('You used the "new" keyword');
    } else {
        console.log('You did NOT use the "new" keyword');
        return;
    }
    // constructor logic here
    // ...
});

答案 19 :(得分:0)

虽然这个帖子很古老,但我很惊讶没有人提到在严格模式下('use strict')函数的默认this值是未定义的,而不像以前那样设置为global / window,因此要检查是否未使用新功能,只需测试falsey !this的值function ctor() { 'use strict'; if (typeof this === 'undefined') console.log('Function called under strict mode (this == undefined)'); else if (this == (window || global)) console.log('Function called normally (this == window)'); else if (this instanceof ctor) console.log('Function called with new (this == instance)'); return this; }   - EG:

this

如果按原样测试该函数,由于函数开头的'use strict'指令,您将未定义为'use strict'值。当然,如果已经有严格模式,则删除this指令时不会更改,但如果删除它,则window值将设置为global或{ {1}}。 如果您使用new来调用该函数,那么this值将匹配instanceof检查(尽管如果您检查了其他内容,则实例是最后一个选项,因此不需要此检查,并且要避免如果你想继承实例的话)

function ctor() { 'use strict';
  if (!this) return ctor.apply(Object.create(ctor.prototype), arguments);
  console.log([this].concat([].slice.call(arguments)));
  return this;
}

这会将this值以及传递给该函数的任何参数记录到控制台,并返回this值。如果this值为falsey,那么它会使用Object.create(ctor.prototype)创建一个新实例,并使用Function.apply()使用相同的参数重新调用构造函数,但使用正确的实例{{1} 1}}。如果this值不是this,那么它将被假定为有效实例并返回。

答案 20 :(得分:0)

Tim Down我认为是对的。我认为,一旦你认为你需要能够区分两种呼叫模式,那么你就不应该使用“this”关键字。 this不可靠,它可能是全局对象,也可能是一些完全不同的对象。事实上,具有这些不同激活模式的功能,其中一些功能按照您的意图工作,其他功能完全是疯狂的,这是不可取的。我想也许你正试图弄清楚这一点。

有一种惯用的方法来创建一个构造函数,无论它如何被调用,它的行为都是相同的。是否像Thing(),new Thing()或foo.Thing()。它是这样的:

function Thing () {
   var that = Object.create(Thing.prototype);
   that.foo="bar";
   that.bar="baz";
   return that;
}

其中Object.create是一个新的ecmascript 5标准方法,可以在这样的常规javascript中实现:

if(!Object.create) {
    Object.create = function(Function){
        // WebReflection Revision
       return function(Object){
           Function.prototype = Object;
           return new Function;
    }}(function(){});
}

Object.create将一个对象作为参数,并返回一个新对象,该对象使用在对象中传递的对象作为其原型。

但是,如果你真的试图让一个函数的行为有所不同,这取决于它的调用方式,那么你就是一个坏人,你不应该编写javascript代码。

答案 21 :(得分:0)

如果您不想在对象中放置__previouslyConstructedByX属性 - 因为它污染了对象的公共接口并且很容易被覆盖 - 只是不要返回x的实例:< / p>

function x() {

    if(this instanceof x) {
        console.log("You invoked the new keyword!");
        return that;
    }
    else {
        console.log("No new keyword");
        return undefined;
    }

}

x();
var Z = new x(); 
Z.lolol = x; 
Z.lolol();
new Z.lolol();

现在x函数永远不会返回x类型的对象,因此(我认为)this instanceof x仅在使用new关键字调用函数时才求值为true

缺点是这有效地搞砸了instanceof的行为 - 但取决于你使用它(我不倾向于)多少可能不是问题。


如果您的目标是两个案例都返回30,则可以返回Number的实例,而不是x的实例:

function x() {

    if(this instanceof x) {
        console.log("You invoked the new keyword!");
        var that = {};
        return new Number(30);
    }
    else {
        console.log("No new");
        return 30;
    }

}

console.log(x());
var Z = new x();
console.log(Z);
Z.lolol = x;
console.log(Z.lolol());
console.log(new Z.lolol());