web-dev-qa-db-ja.com

関数がコンストラクターとして呼び出されるかどうかを検出する方法は?

与えられた関数:

_function x(arg) { return 30; }
_

次の2つの方法で呼び出すことができます。

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

最初は30を返し、2番目はオブジェクトを返します。

どのように関数が呼び出されたかを検出するにはどうすればいいですか?関数自体の内部

ソリューションが何であれ、次の呼び出しでも機能する必要があります。

_var Z = new x(); 
Z.lolol = x; 
Z.lolol();
_

現在、すべてのソリューションは、Z.lolol()がコンストラクタとしてそれを呼び出していると考えています。

103
Claudiu

注:これはES2015以降で可能になりました。 ダニエル・ワイナーの答えを参照

あなたが望むことは可能だとは思いません[ES2015以前]。信頼できる推論を行うのに十分な情報が関数内にありません。

ECMAScript第3版の仕様を見ると、new x()が呼び出されたときに実行される手順は基本的に次のとおりです。

  • 新しいオブジェクトを作成する
  • 内部[[Prototype]]プロパティをxのprototypeプロパティに割り当てます
  • 通常どおりxを呼び出し、新しいオブジェクトをthisとして渡します
  • xの呼び出しがオブジェクトを返した場合はそれを返し、そうでない場合は新しいオブジェクトを返します

関数の呼び出し方法に関する有用なものは実行中のコードで利用できないため、x内でテストできるのはthis値のみです。これがここでのすべての答えです。 。ご覧のように、コンストラクターとしてxを呼び出したときに、* xの新しいインスタンスは、xとして渡されたthisの既存のインスタンスと区別できません。 _ xを関数として呼び出すとき、unlessxによって作成されたすべての新しいオブジェクトにプロパティを割り当てます構築されます:

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によって構築されたすべてのオブジェクトに余分な無用のプロパティがあり、それが上書きされる可能性があるからです。

(*)「instance of」は不正確な用語ですが、十分に近く、「xコンストラクターとして」

88
Tim Down

ECMAScript 6以降、これは new.targetnew.targetは、関数がnew(またはReflect.constructnew)のように機能します。それ以外の場合は、undefinedです。

function 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"
64
Daniel Weiner

1)this.constructor

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

2)はい、newコンテキストで使用された場合、戻り値は破棄されます

52
Greg

注:この答えは、2008に書かれていましたが、javascriptはES3から1999。それ以来、多くの新しい機能が追加されているため、より良いソリューションが存在します。この答えは歴史的な理由で保持されています。

以下のコードの利点は、関数の名前を2回指定する必要がなく、匿名関数でも機能することです。

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

Update下のコメントで claudi が指摘しているように、コンストラクターを作成した同じオブジェクトにコンストラクターを割り当てると、上記のコードは機能しません。私はそれを行うコードを書いたことがなく、他の誰かがそれを行うのを見たことがあります。

クラウディウスの例:

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のネイティブサポートはありません。これは私がこれまでに考えた中で最も近いものであり、プロパティを削除しない限り機能するはずです。

19
some

基本的に同じ内部の2つの方法。 thisのスコープをテストするか、this.constructorです。

メソッドをコンストラクターとして呼び出した場合、thisはクラスの新しいインスタンスになり、メソッドとしてメソッドを呼び出した場合、thisはメソッドのコンテキストオブジェクトになります。同様に、オブジェクトのコンストラクターは、新規として呼び出された場合はメソッド自体になり、そうでない場合はシステムオブジェクトコンストラクターになります。それは泥として明らかですが、これは役立つはずです:

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
  }
}
8
annakata

コンストラクタ内で[this]のインスタンスタイプを確認するのが方法です。問題は、これ以上苦労せずにこのアプローチがエラーを起こしやすいことです。ただし、解決策があります。

関数ClassA()を扱っているとしましょう。初歩的なアプローチは次のとおりです。

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

上記のソリューションが期待どおりに機能しないといういくつかの手段があります。次の2つだけを検討してください。

    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]値でだまそうとする試みがすべて効果的に防止されます。バインドされた関数は、new演算子で呼び出さない限り、常に元の[this]コンテキストを保持します。

高度なバージョンでは、任意のオブジェクトにコンストラクターを適用する機能が提供されます。いくつかの用途は、コンストラクターを型コンバーターとして使用することや、継承シナリオで基本クラスコンストラクターの呼び出し可能なチェーンを提供することです。

    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");
            }
        });
5

実際には、解決策は非常に可能で簡単です...なぜそんなに多くの言葉がこんなに小さなもののために書かれているのか理解できない

UPDATE: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/

4
ymz

JavaScriptコードで関数がどのように呼び出されるかを区別する信頼できる方法はありません。1

ただし、関数呼び出しではthisがグローバルオブジェクトに割り当てられ、コンストラクターではthisが新しいオブジェクトに割り当てられます。この新しいオブジェクトをグローバルオブジェクトにすることはできません。実装でグローバルオブジェクトを設定できる場合でも、それを行う機会がまだないためです。

グローバルオブジェクトを取得するには、thisを返す関数(heh)として呼び出される関数を使用します。

私の直感では、ECMAScript 1.3の仕様では、関数として呼び出されたときの動作が定義されているコンストラクターは、この比較を使用して呼び出された方法を区別することになっています。

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

とにかく、誰でも関数またはコンストラクタのcallまたはapplyを使用して、thisを任意に設定できます。ただし、この方法では、グローバルオブジェクトの「初期化」を回避できます。

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

1。 ホスト(実装とも呼ばれます)は、内部プロパティ[[Call]]および[[Construct]]。前者は関数式またはメソッド式に対して呼び出され、後者はnew式に対して呼び出されます。

3
acelent

このスレッドを見るまで、コンストラクタがインスタンスのプロパティであるとは考えませんでしたが、次のコードはそのまれなシナリオをカバーしていると思います。

// 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をチェックするだけです。

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
3
Frunsi

http://packagesinjavascript.wordpress.com/ のテストで、すべての場合で(この==ウィンドウ)がクロスブラウザで動作するかどうかのテストが見つかりました。 。

-Stijn

2
Stijn

ジョン・レジグから:

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
2
haijin

あなたがハックするつもりなら、instanceofが他の回答と同様にnew.targetの後の最小の解決策です。しかし、instanceofソリューションを使用すると、この例では失敗します。

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

@TimDownソリューションと組み合わせて、古いECMAScriptバージョンとの互換性が必要な場合、ES6のWeakSetを使用して、インスタンス内にプロパティを配置できません。さて、WeakSetは未使用のオブジェクトをガベージコレクションできるようにするために使用されます。 new.targetは、ES6の構文機能であるため、同じソースコードでは互換性がありません。 ECMAScriptでは、識別子は予約語の1つにはできず、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演算子は 指定 として:

11.8.6 instanceof演算子
---プロダクションRelationalExpression:RelationalExpression instanceof ShiftExpressionは次のように評価されます。
--- 1. RelationalExpressionを評価します。
--- 2. GetValue(Result(1))を呼び出します。
--- 3. ShiftExpressionを評価します。
--- 4. GetValue(Result(3))を呼び出します。
--- 5. Result(4)がオブジェクトでない場合、 TypeError 例外。
--- 6. Result(4)に[[HasInstance]]メソッドがない場合、 TypeError 例外。
--- 7. Result(4)の[[HasInstance]]メソッドをパラメーターResult(2)で呼び出します。
--- 8. Result(7)を返します。
15.3.5.3 [[HasInstance]](V)
--- FがFunctionオブジェクトであると仮定します。
--- Fの[[HasInstance]]メソッドが値Vで呼び出されると、次の手順が実行されます。
--- 1. Vがオブジェクトでない場合、リターン false
--- 2.プロパティ名でFの[[Get]]メソッドを呼び出す "プロトタイプ"
--- 3. OをResult(2)とします。
--- 4. Oがオブジェクトでない場合、 TypeError 例外。
--- 5. Vを[[Prototype]]プロパティの値とします。
--- 6. Vが** null **の場合、リターン false
--- 7. OとVが同じオブジェクトを参照する場合、または互いに結合されたオブジェクトを参照する場合(13.1.2)、 本当
--- 8.手順5に進みます。

そして、それは、オブジェクトにならないか、指定された[[HasInstance]]メソッドを持つ右側のオブジェクトのプロトタイプに等しくなるまで、プロトタイプに行った後、左側の値を再帰することを意味します。つまり、左手側が右手側のインスタンスであるかどうかをチェックし、左手側のすべての内部プロトタイプを消費します。

function x() {
    if (this instanceof x) {
        /* Probably invoked as constructor */
    } else return 30;
}
2
Hydroper

たぶん私は間違っていますが、(寄生虫を犠牲にして)次のコードは解決策のように思えます:

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
1
fedeghe

つかいます this instanceof arguments.callee(オプションでarguments.calleeは、それが含まれている関数を使用してパフォーマンスを向上させます)、何かがコンストラクターとして呼び出されるかどうかを確認します。 しない use this.constructorは簡単に変更できます。

0
Eli Grey

オブジェクトの代わりに文字列を返す関数を実装しようとしたときに、同じ問題が発生しました。

関数の先頭に「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()関数に変更することにしました。そのため、インスタンス化を試行しないため、安全性チェックはまったく必要ありません。しかし、それはあなたがやろうとしていることに依存します。

0
Diogo Schneider

質問の一番上で、以下のコードは、関数が新しい関数なしで呼び出された場合の問題を自動修正します。

function Car() {

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

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

}
let c1 = new Car();
console.log(c1);
0
Sumer
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
    // ...
});
0
Joshua Wise

このスレッドは古くからありますが、ストリクトモード( _'use strict'_ )で関数のデフォルトのthis値が以前のようにグローバル/ウィンドウに設定されるのではなく未定義であることを誰も言及していないことに驚いています。したがって、newが使用されていないかどうかを確認するには、_!this_のfalsey値をテストするだけです-EG:

_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; 
}
_

その関数をそのままテストすると、関数の先頭にある_'use strict'_ディレクティブにより、this値として未定義になります。もちろん、すでに厳格モードがオンになっている場合、_'use strict'_ディレクティブを削除しても変更されませんが、そうでない場合はthis値がwindowまたはglobalに設定されます。 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()を使用して、thisと同じインスタンスで同じパラメーターでコンストラクターを再呼び出しします。 thisの値がfalsey以外の場合、有効なインスタンスと見なされて返されます。

0
BoB

ティムダウン私は正しいと思います。 2つの呼び出しモードを区別する必要があると思われる時点に達したら、「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は、次のような通常のjavascriptで実装できる新しいecmascript 5標準メソッドです。

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

Object.createはオブジェクトをパラメーターとして受け取り、オブジェクトとして渡された新しいオブジェクトをプロトタイプとして返​​します。

ただし、関数の呼び出し方法に応じて関数の動作を実際に変えようとしている場合は、あなたは悪人であり、javascriptコードを書くべきではありません。

0
Breton

オブジェクトに__previouslyConstructedByXプロパティを配置したくない場合-オブジェクトのパブリックインターフェイスを汚染し、簡単に上書きされる可能性があるため-xのインスタンスを返さないでください。

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型のオブジェクトを返さないため、newキーワードを使用して関数が呼び出された場合にのみthis instanceof xは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());
0