web-dev-qa-db-fra.com

Comment détecter si une fonction est appelée en tant que constructeur?

Étant donné une fonction:

function x(arg) { return 30; }

Vous pouvez l'appeler de deux manières:

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

Le premier retourne 30, le second retourne un objet.

Comment pouvez-vous détecter comment la fonction a été appelée dans 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 Z.lolol() l'appelle en tant que constructeur.

98
Claudiu

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

Je ne pense pas que ce que vous voulez soit possible [avant ES2015]. Il n’ya tout simplement pas assez d’informations disponibles dans la fonction pour permettre une déduction fiable.

En regardant la spécification ECMAScript 3ème édition, les étapes suivies lorsque new x() est appelé sont essentiellement les suivantes:

  • Créer un nouvel objet
  • Affectez 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 renvoyé un objet, retournez-le, sinon renvoyez 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, aussi la seule chose qu’il est possible de tester dans x est la valeur this, qui correspond à toutes les réponses données ici. Comme vous l'avez observé, une nouvelle instance de * x lors de l'appel de x en tant que constructeur est impossible à distinguer d'une instance préexistante de x passée en tant que this lors de l'appel de x en tant que fonction, sauf si vous affectez une propriété à chaque nouvelle. objet créé par x lors 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, car vous avez maintenant une propriété supplémentaire inutile sur chaque objet construit par x qui pourrait être écrasée, mais je pense que c’est le mieux que vous puissiez faire.

(*) "instance of" est un terme imprécis mais suffisamment proche et plus concis que "objet créé en appelant x en tant que constructeur"

85
Tim Down

A partir de ECMAScript 6, cela est possible avec new.target . new.target sera défini si la fonction est appelée avec new (ou avec Reflect.construct, qui agit comme new), sinon c'est 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"
59
Daniel Weiner

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 le contexte new

51
Greg

REMARQUE: cette réponse a été écrite dans 2008, alors que javascript était encore dans ES3 à partir de 1999. De nombreuses nouvelles fonctionnalités ont été ajoutées depuis, de sorte que de meilleures solutions existent maintenant. Cette réponse est conservée pour des raisons historiques. 

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

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

Update Comme claudiu l'a souligné dans un commentaire ci-dessous, le code ci-dessus ne fonctionne pas si vous affectez le constructeur au même objet qu'il a créé. Je n'ai jamais écrit de code qui fait cela et j'ai plus récemment vu quelqu'un d'autre faire ça. 

Exemple 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 cependant l'écraser avec n'importe quelle valeur, y compris undefined, et cela fonctionne toujours Mais si vous le supprimez, il se cassera.

Pour l'instant, ecmascript ne prend pas en charge nativement la détection si une fonction a été appelée en tant que constructeur. C’est la chose la plus proche que j’ai proposée jusqu’à présent, et elle devrait fonctionner à moins que vous ne supprimiez la propriété.

19
some

Deux façons, essentiellement les mêmes sous le capot. Vous pouvez tester quelle est 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 en tant que méthode, this sera l'objet contextuel 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 nouveau et le constructeur du système, sinon. C'est clair comme de la boue, 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
  }
}
8
annakata

Vérifier le type d'instance de [this] dans le constructeur est la voie à suivre. Le problème est que, sans plus tarder, cette approche est sujette aux erreurs. Il y a une solution cependant.

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

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

Il existe plusieurs moyens pour que la solution susmentionnée ne fonctionne pas comme prévu. Considérez juste ces deux:

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

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

Pour les surmonter, certains suggèrent soit de placer a) des informations dans un champ de l'instance, comme "ConstructorFinished" et de vérifier ces informations, ou b) de garder une trace de vos objets construits dans une liste. Je ne suis pas à l'aise avec les deux, car la modification de chaque instance de ClassA est beaucoup trop invasive et coûteuse pour qu'une fonctionnalité liée au type fonctionne. La collecte de tous les objets d'une liste peut générer des problèmes de récupération de place et de ressources si ClassA aura plusieurs instances.

La meilleure chose à faire est de pouvoir contrôler l’exécution de votre fonction ClassA. L'approche simple est:

    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 toutes les tentatives de tromper avec la valeur [this]. Une fonction liée conservera toujours son contexte [this] d'origine à moins que vous ne l'appeliez avec l'opérateur new

La version avancée donne la possibilité d'appliquer le constructeur sur des objets arbitraires. Certaines utilisations peuvent utiliser le constructeur en tant que convertisseur de type ou fournir une chaîne appelable de constructeurs de classe de base 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");
            }
        });
5
Peter Aron Zentai

en fait, la solution est très simple et possible… je ne comprends pas pourquoi tant de mots ont été écrits pour une chose aussi minuscule

UPDATE: grâce à TwilightSun, la solution est maintenant terminée, même pour le test Claudiu suggéré! Merci les gars!!!

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"

démontré ici: https://jsfiddle.net/9cqtppuf/

4
ymz

Il n'y a pas de moyen fiable de distinguer comment une fonction est appelée en code JavaScript.1

Toutefois, this sera attribué à un appel de fonction dans l'objet global, tandis que this sera attribué à un nouvel objet par un constructeur. Ce nouvel objet ne peut jamais être l'objet global, car même si une implémentation vous permet de définir l'objet global, vous n'avez toujours pas eu la possibilité de le faire.

Vous pouvez obtenir l'objet global en ayant une fonction appelée en tant que fonction (heh) retournant this.

Mon intuition est que, dans la spécification d'ECMAScript 1.3, les constructeurs qui ont un comportement défini pour lorsqu'ils sont appelés en tant que fonction sont supposés distinguer comment ils ont été appelés à l'aide de cette comparaison:

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

Quoi qu'il en soit, n'importe qui peut simplement utiliser call ou apply d'une fonction ou d'un constructeur et définir this à n'importe quoi. Mais de cette façon, vous pouvez éviter d’initialiser l’objet global:

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

1. L'hôte (alias implémentation) peut être capable de faire la différence s'il implémente l'équivalent des propriétés internes [[Call]] et [[Construct]]. Le premier est appelé pour les expressions de fonction ou de méthode, tandis que le dernier est appelé pour les expressions new.

3
acelent

Jusqu'à ce que je voie ce fil, je n'avais jamais pensé que le constructeur pouvait être une propriété d'une instance, mais je pense que le code suivant couvre ce scénario rare.

// 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"

Dans la plupart des cas, je ne ferai probablement que vérifier instanceof.

3
please delete me

En prolongeant la solution Gregs, celle-ci fonctionne parfaitement avec les cas de test que vous avez fournis:

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

EDIT: ajout de quelques cas de tests

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

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

Si vous allez bidouiller, alors instanceof est la solution minimale après new.target comme pour les autres réponses. Mais en utilisant la solution instanceof, cela échouerait avec cet exemple:

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

En vous associant à la solution @TimDown, vous pouvez utiliser la variable WeakSet de ES6 si vous souhaitez une compatibilité avec les anciennes versions d'ECMAScript afin d'éviter de placer des propriétés dans des instances. WeakSet sera utilisé afin de permettre aux objets inutilisés d'être récupérés. new.target ne sera pas compatible dans le même code source, car il s'agit d'une fonctionnalité de syntaxe d'ES6. ECMAScript spécifie que les identificateurs ne peuvent pas être l'un des mots réservés et que new n'est en aucun cas un objet.

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

L'opérateur instanceof de ECMAScript 3 est spécifié en tant que:

11.8.6 L'opérateur instanceof
--- La production RelationalExpression: RelationalExpression exemple de ShiftExpression est évaluéas suit:
--- 1. Evaluer RelationalExpression.
--- 2. Appelez GetValue (Résultat (1)).
--- 3. Evaluez ShiftExpression.
--- 4. Appelez GetValue (Résultat (3)).
--- 5. Si Result (4) n'est pas un objet, lancez un Erreur-type exception.
--- 6. Si Result (4) n’a pas de méthode [[HasInstance]], lance un Erreur-type exception.
--- 7. Appelez la méthode [[HasInstance]] de Result (4) avec le paramètre Result (2).
--- 8. Résultat (7) .
15.3.5.3 [[HasInstance]] (V)
--- Supposer que F est un objet Function.
--- Lorsque la méthode [[HasInstance]] de F est appelée avec la valeur V, les étapes suivantes sont entreprises:
--- 1. Si V n'est pas un objet, retourne false.
--- 2. Appelez la méthode [[Get]] de F avec le nom de la propriété "prototype".
--- 3. Soit O le résultat (2).
--- 4. Si O n'est pas un objet, lancez un Erreur-type exception.
--- 5. Soit V la valeur de la propriété [[Prototype]] de V.
--- 6. Si V est ** null **, retourne false.
--- 7. Si O et V se rapportent au même objet ou s’ils se rapportent à des objets joints entre eux (13.1.2), retourne vrai.
--- 8. Passez à l’étape 5.

Cela signifie que la valeur du côté gauche sera récurrente après le passage à son prototype jusqu'à ce qu'il ne s'agisse pas d'un objet ou jusqu'à ce qu'il soit égal au prototype de l'objet de droite avec la méthode [[HasInstance]] spécifiée. Cela signifie que cela vérifiera si le côté gauche est une instance du côté droit, consommant tous les prototypes internes du côté gauche.

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

peut-être que je me trompe mais (au prix d'un parasite) le code suivant semble être une solution:

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

Lors de mes tests pour http://packagesinjavascript.wordpress.com/ , j'ai trouvé le test si (cette == fenêtre) fonctionnait dans tous les cas avec plusieurs navigateurs;.

-Stijn

1
Stijn

Utilisez this instanceof arguments.callee (en remplaçant éventuellement arguments.callee par la fonction dans laquelle il se trouve, ce qui améliore les performances) pour vérifier si quelque chose est appelé comme constructeur. Ne pas utilisez this.constructor car cela peut être facilement modifié.

0
Eli Grey

J'ai eu ce même problème lorsque j'ai essayé d'implémenter une fonction qui renvoie une chaîne au lieu d'un objet.

Il semble suffisant de vérifier l’existence de "ceci" au début de votre fonction:

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

Quoi qu'il en soit, j'ai finalement décidé de transformer ma pseudo-classe RGB () en une fonction rgb (). Je ne tenterai donc pas de l'instancier, ce qui ne nécessite aucune vérification de sécurité. Mais cela dépend de ce que vous essayez de faire.

0
Diogo Schneider

En haut de la question, le code ci-dessous corrigera automatiquement le problème si la fonction est appelée sans nouvelle.

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

Bien que ce fil soit ancien, je suis surpris que personne n'ait mentionné qu'en mode strict ( 'use strict' ) la valeur par défaut this d'une fonction est indéfinie, au lieu d'être définie sur global/window comme auparavant, afin de vérifier si new n'est pas nouveau utilisé teste simplement falsey valeur de !this - PAR EXEMPLE:

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

Si vous testez cette fonction en l'état, la valeur this sera indéfinie, en raison de la directive 'use strict' au début de la fonction. Bien sûr, si le mode strict est déjà activé, il ne changera pas si vous supprimez la directive 'use strict', mais sinon, la valeur this sera définie sur window ou global. Si vous utilisez new pour appeler la fonction alors la valeur this correspondra à l'instance de la vérification (bien que si vous avez coché les autres éléments, instance est la dernière option, cette vérification n'est donc pas nécessaire et à éviter si vous souhaitez quand même hériter d'instances)

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

Ceci enregistrera la valeur this et tous les arguments que vous transmettez à la fonction à console et renverra la valeur this. Si la valeur this est falsey, il crée une nouvelle instance à l'aide de Object.create(ctor.prototype) et utilise Function.apply() pour appeler à nouveau le constructeur avec les mêmes paramètres, mais avec l'instance correcte comme this. Si la valeur this est différente de falsey, elle est supposée être une instance valide et renvoyée.

0
BoB

Tim Down je pense est correct. Je pense qu'une fois que vous en êtes arrivé au point où vous pensez devoir pouvoir distinguer les deux modes d'appel, vous ne devez pas utiliser le mot clé "this". this n'est pas fiable et il peut s'agir d'un objet global ou d'un objet complètement différent. Le fait est qu’il est indésirable d’avoir une fonction avec ces différents modes d’activation, dont certains fonctionnent comme vous le souhaitez, d’autres font quelque chose de totalement sauvage. Je pense que vous essayez peut-être de comprendre cela à cause de cela.

Il existe un moyen idiomatique de créer une fonction constructeur qui se comporte de la même manière, quelle que soit sa dénomination. que ce soit comme Thing (), new Thing () ou foo.Thing (). Ça va comme ça:

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

où Object.create est une nouvelle méthode standard ecmascript 5 qui peut être implémentée en javascript classique, comme ceci:

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

Object.create prendra un objet en tant que paramètre et retournera un nouvel objet avec celui transmis dans object comme prototype. 

Toutefois, si vous essayez vraiment de faire en sorte qu'une fonction se comporte différemment selon sa dénomination, vous êtes une mauvaise personne et vous ne devez pas écrire de code javascript.

0
Breton

Si vous ne souhaitez pas placer une propriété __previouslyConstructedByX dans l'objet, car il pollue l'interface publique de l'objet et peut facilement être écrasée. Ne renvoyez pas simplement une instance de 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();

Maintenant, la fonction x ne retourne jamais un objet de type x, donc (je pense que) this instanceof x n'évalue que si la fonction est appelée avec le mot clé new.

L'inconvénient est que cela perturbe efficacement le comportement de instanceof - mais cela dépend de l'utilisation que vous en faites (je n'ai pas tendance à le faire).


Si votre objectif est de renvoyer 30 dans les deux cas, vous pouvez renvoyer une instance de Number au lieu d'une instance de 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
Richard JP Le Guen