116 votes

Comment détecter si une fonction est appelée comme constructeur ?

Étant donné une fonction :

function x(arg) { return 30; }

Vous pouvez l'appeler de deux façons :

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

Le premier renvoie 30, le second renvoie un objet.

Comment pouvez-vous détecter de quelle manière la fonction a été appelée ? à l'intérieur de la fonction elle-même ?

Quelle que soit votre solution, elle doit également fonctionner avec l'invocation suivante :

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

Toutes les solutions pensent actuellement que le Z.lolol() l'appelle comme un constructeur.

94voto

Tim Down Points 124501

NOTE : Ceci est maintenant possible dans ES2015 et les versions ultérieures. Voir La réponse de Daniel Weiner .

Je ne pense pas que ce que vous voulez soit possible [avant ES2015]. Il n'y a tout simplement pas assez d'informations disponibles dans la fonction pour faire une inférence fiable.

Si l'on examine la spécification ECMAScript 3e édition, on constate que les mesures prises pour new x() est appelé sont essentiellement :

  • Créer un nouvel objet
  • Assignez sa propriété interne [[Prototype]] à la propriété prototype de x
  • Appelez x comme d'habitude, en lui passant le nouvel objet comme this
  • Si l'appel à x a retourné un objet, le retourner, sinon retourner le nouvel objet

Rien d'utile sur la façon dont la fonction a été appelée n'est mis à la disposition du code en cours d'exécution, donc la seule chose qu'il est possible de tester à l'intérieur de la fonction x est le this et c'est ce que font toutes les réponses ici. Comme vous l'avez observé, une nouvelle instance de* x lors de l'appel x en tant que constructeur est indiscernable d'une instance préexistante de x passé en tant que this lors de l'appel x comme une fonction, sauf si vous attribuez une propriété à chaque nouvel objet créé par x au fur et à mesure de sa construction :

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

Évidemment, ce n'est pas idéal, puisque vous avez maintenant une propriété supplémentaire inutile sur chaque objet construit par x qui pourrait être écrasé, mais je pense que c'est le mieux que l'on puisse faire.

(*) "instance de" est un terme inexact mais suffisamment proche, et plus concis que "objet qui a été créé en appelant x comme un constructeur"

0 votes

Pourquoi utilisez-vous this.__previouslyConstructedByX au lieu de var previouslyConstructedByX ?

1 votes

@PiPeep : Seulement comme une convention de dénomination destinée à suggérer que la propriété ne doit pas être utilisée à d'autres fins. Certaines personnes utilisent une convention selon laquelle les propriétés qui ne sont pas destinées à faire partie de l'API publique d'un objet commencent par un trait de soulignement, tandis qu'il existe des propriétés non standard d'objets JavaScript dans certains navigateurs qui commencent par un double trait de soulignement et je m'inspirais de ces idées. Je n'y ai pas réfléchi en détail et je pourrais être persuadé que les deux traits de soulignement sont une mauvaise idée.

0 votes

Je pense que c'est la bonne réponse. Dans le cas général, vous ne pouvez tout simplement pas le faire.

53voto

Greg Points 132247

1) Vous pouvez vérifier this.constructor :

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

2) Oui, la valeur de retour est simplement supprimée lorsqu'elle est utilisée dans la fonction new contexte

1 votes

Ah, bien. Est-ce que ce constructeur est indéfini dans le "else" ?

0 votes

Non, c'est un constructeur d'objet par défaut parce que le contexte de "ceci" dans ce cas est la fonction x elle-même, comme vous pouvez le voir dans ma réponse redondante ci-dessous, que je n'aurais pas postée si SO avait indiqué que la réponse avait déjà été donnée. soupir

0 votes

@annakata : pas de soucis, votre réponse est toujours valable. upvote de ma part en tout cas.

19voto

some Points 18965

NOTE : Cette réponse a été rédigée en 2008 quand javascript était encore en vigueur ES3 de 1999 . De nombreuses fonctionnalités ont été ajoutées depuis lors, de sorte que de meilleures solutions existent désormais. Cette réponse est conservée pour des raisons historiques.

L'avantage du code ci-dessous est que vous n'avez pas besoin de spécifier le nom de la fonction deux fois et qu'il fonctionne également pour les fonctions anonymes.

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

Update Comme claudiu ont fait remarquer dans un commentaire ci-dessous, le code ci-dessus ne fonctionne pas si vous assignez le constructeur au même objet qu'il a créé. Je n'ai jamais écrit de code qui fait cela et je n'ai jamais vu personne d'autre le faire.

Exemple de Claudius :

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

En ajoutant une propriété à l'objet, il est possible de détecter si l'objet a été initialisé.

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

Même le code ci-dessus sera cassé si vous supprimez la propriété ajoutée. Vous pouvez toutefois l'écraser avec la valeur de votre choix, notamment undefined et ça marche toujours. Mais si vous le supprimez, il se cassera.

Il n'existe actuellement aucun support natif dans ecmascript pour détecter si une fonction a été appelée en tant que constructeur. C'est la solution la plus proche que j'ai trouvée jusqu'à présent, et elle devrait fonctionner à moins que vous ne supprimiez la propriété.

1 votes

Ça ne marche pas. Regardez : { var Z = new x() ; Z.lolol = x ; Z.lolol();}. Il pense que la 2ème invocation est appelée en tant que constructeur.

2 votes

Cela ne semble pas être autorisé par "use strict"; .

3 votes

@MattFenwick Correct, puisque arguments.callee est interdite en mode strict. Si vous remplacez cela par le nom du constructeur, cela devrait fonctionner.

8voto

annakata Points 42676

Deux voies, essentiellement les mêmes sous le capot. Vous pouvez tester ce que la portée de this ou vous pouvez tester ce que this.constructor est.

Si vous avez appelé une méthode en tant que constructeur this sera une nouvelle instance de la classe, si vous appelez la méthode comme une méthode this sera l'objet de contexte des méthodes. De même, le constructeur d'un objet sera la méthode elle-même si elle est appelée en tant que new, et le constructeur Object du système sinon. C'est clair comme de l'eau de roche, mais cela devrait aider :

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

0 votes

Typeof (this) sera toujours "object" dans vos exemples.

0 votes

Bah, je m'avance un peu, je devrais juste tester 'ceci' directement - édité

1 votes

Notez que this.constructor pour un appel non nouveau est DOMWindow dans Chrome, et indéfini dans IE , donc vous ne pouvez pas compter dessus.

5voto

Peter Aron Zentai Points 3760

Il faut vérifier le type d'instance de [this] dans le constructeur. Le problème est que, sans autre précision, cette approche est source d'erreurs. Il existe toutefois une solution.

Disons que nous avons affaire à la fonction ClassA(). L'approche rudimentaire est la suivante :

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

Il existe plusieurs raisons pour lesquelles la solution susmentionnée ne fonctionnera pas comme prévu. Considérez seulement les deux suivantes :

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

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

Pour y remédier, certains suggèrent soit a) de placer une information dans un champ de l'instance, comme "ConstructorFinished" et de la vérifier, soit b) de garder une trace de vos objets construits dans une liste. Je ne suis pas à l'aise avec ces deux solutions, car modifier chaque instance de ClassA est bien trop invasif et coûteux pour qu'une fonctionnalité liée au type fonctionne. Rassembler tous les objets dans une liste pourrait poser des problèmes de garbage collection et de ressources si ClassA a de nombreuses instances.

La solution consiste à pouvoir contrôler l'exécution de votre fonction ClassA. L'approche simple est la suivante :

    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();

Cela empêchera efficacement toute tentative de fraude avec la valeur [this]. Une fonction liée conservera toujours son contexte [this] original, sauf si vous l'appelez avec l'option nouveau opérateur.

La version avancée donne la possibilité d'appliquer le constructeur sur des objets arbitraires. On peut par exemple utiliser le constructeur comme convertisseur de type ou fournir une chaîne de constructeurs de classes de base appelables dans des scénarios d'héritage.

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

0 votes

Bien sûr, cela ne fonctionnera que dans les navigateurs qui ont une implémentation native de .bind, c'est-à-dire les navigateurs qui supportent ecmascript 5, mais il semble que vous ayez ici la seule solution qui résout le problème sans ajouter de propriétés supplémentaires à l'objet.

0 votes

Je voulais ajouter du code pour montrer comment votre solution résout le problème spécifique, mais apparemment c'est plus approprié dans un commentaire (désolé que les commentaires ne montrent pas bien le code) : var x = function x(arg) { /* note: strict mode deprecates arguments.callee, but you can use the function name here. */ if (this instanceof x == false) { /* called as a function */ return 30; } }.bind({}); var Z = new x(); console.log(Z); Z.lolol = x; console.log(Z.lolol());

0 votes

Bien entendu, une variante de cette solution consiste à conserver une référence à l'objet auquel le constructeur est lié, puis à effectuer une vérification === par rapport à cet objet au lieu de l'instanceof.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X