2009-12-29 2 views
2

Une mauvaise saisie en javascript est d'oublier d'appeler new sur une fonction destinée à être instanciée, conduisant à this être lié à un objet différent (généralement le global) au lieu d'un nouveau. Une solution que j'ai lu est sur le point de vérifier explicitement dans la fonction constructeur en utilisant l'idiome suivant:Suppression de la nécessité de "nouveau"

function SomeConstructor(x, y, ...) { 
    // check if `this` is created with new 
    if (!(this instanceof arguments.callee)) 
     return new SomeConstructor(x, y, ...); 
    // normal initialization code follows 

maintenant new SomeConstructor(...) et SomeConstructor(...) sont équivalentes.

Je voudrais simplifier cela en créant une fonction wrapper factory(fn) qui fait les deux premières lignes, puis délègue à la fonction enveloppée fn. Ce serait utilisé comme:

SomeConstructor = factory(function (x, y, ...) { 
    // normal initialization code follows 
}) 

Ma première tentative a été:

function factory(fn) { 
    return function() { 
    if (!(this instanceof arguments.callee)) { 
     return new arguments.callee.apply(this, arguments); 
    } 
    fn.apply(this, arguments); 
    } 
} 

mais il échoue avec "appelé Function.prototype.apply sur incompatibles [object Object]". La deuxième tentative a été:

function factory(fn) { 
    return function() { 
    if (!(this instanceof arguments.callee)) { 
     var tmp = new arguments.callee(); 
     arguments.callee.apply(tmp, arguments); 
     return tmp; 
    } 
    fn.apply(this, arguments); 
    } 
} 

Ce genre de travaux, mais il peut appeler deux fois la fonction enveloppée: une fois sans argument (pour créer une nouvelle instance) et une fois avec les arguments transmis pour l'initialisation réelle. Apparemment, c'est fragile et inefficace, mais je ne peux pas trouver un moyen de le faire avec un seul appel. Est-ce possible ?

EDIT: Sur la base de l'approche de bobince, voici un semblable qui fait l'affaire:

function factory(init) { 
    var run_init = true; 
    function constr() { 
     if (!(this instanceof constr)) { 
      run_init = false; 
      var tmp = new constr(); 
      run_init = true; 
      init.apply(tmp, arguments); 
      return tmp; 
     } 
    if (run_init) 
     init.apply(this, arguments); 
    } 
    return constr; 
} 

Quant à savoir si cela est quelque chose qui devrait être encouragé ou non, est discutable. Je viens d'un fond de Python et je pense à new comme bruit (Java) ou wart (Javascript), mais il me manque peut-être quelque chose.

+18

Je vous recommande de ne pas essayer d'éliminer l'utilisation d'une construction de langage simplement parce que vous pourriez accidentellement oublier de l'inclure. On dirait qu'il y a énormément de travail pour ne pas avoir à taper trois caractères de toute façon. –

+1

@Darrell: Il ne s'agit pas tant d'éviter le travail que d'essayer d'utiliser la métaprogrammation pour faciliter la vie de tout le monde. C'est la norme dans les langages dynamiques comme Javascript, Ruby, et al. –

+4

@John Je comprends et je suis tout pour faciliter la vie, mais je ne vois toujours pas comment tout ce code est plus facile que de taper «nouveau». :) –

Répondre

4

Vous pouvez passer une valeur unique dans le constructeur pour le premier appel (avec new) que vous ne voulez pas signifiés l'initialiseur appelé encore:

var _NOINIT= {}; 
function factory(init) { 
    function constr() { 
     if (!(this instanceof constr)) { 
      var inst= new constr(_NOINIT); 
      init.apply(inst, arguments); 
      return inst; 
     } 
     if (arguments[0]!==_NOINIT) 
      init.apply(this, arguments); 
    } 
    return constr; 
} 

Remarque J'ai utilisé une fonction en ligne du nom de le constructeur car arguments.callee va disparaître dans le mode 'strict' d'ECMAScript Fifth Edition. Cependant, si vous utilisez une fabrique de classe, je suggère de faire de la fonction initialiser un membre de la classe, plutôt que d'être transmis. Ainsi, vous pouvez sous-classer une classe de base et faire en sorte que la sous-classe hérite de l'initialisateur. est un comportement normal dans les langages basés sur les classes. par exemple .:

Function.prototype.makeSubclass= function() { 
    function constr() { 
     var that= this; 
     if (!(this instanceof constr)) 
      that= new constr(_NOINIT); 
     if (arguments[0]!==_NOINIT && '_init' in that) 
      that._init.apply(that, arguments); 
     return that; 
    } 

    if (this!==Object) 
     constr.prototype= new this(_NOINIT); 
    return constr; 
}; 

var Shape= Object.makeSubclass(); 
Shape.prototype._init= function(x, y) { 
    this.x= x; 
    this.y= y; 
}; 

var Point= Shape.makeSubclass(); 
// inherits initialiser(x, y), as no need for anything else in there 

var Circle= Shape.makeSubclass() 
Circle.prototype._init= function(x, y, r) { 
    Shape.prototype._init.call(this, x, y); 
    this.r= r; 
}; 

Bien sûr, vous ne devez pas mettre cela dans le prototype de fonction ... il est une question de goût, vraiment. Comme le permet les constructeurs sans new. Personnellement, je préfère lancer une erreur plutôt que de la faire fonctionner silencieusement, pour essayer de décourager les appels au constructeur nu, car c'est une erreur ailleurs et cela peut rendre le code un peu moins clair.

+0

Merci, cela fait l'affaire! Basé sur la première approche, je suis venu avec un semblable qui utilise une fermeture et une variable booléenne pour contrôler l'initialisation (voir mise à jour) – gsakkis

+0

Yep, semble être un bon hack! – bobince

2

Je n'aime pas votre mélange de arguments.callee et l'identifiant de la fonction. En outre, vous êtes stupide le problème original. Vous auriez dû utiliser apply pour commencer afin de ne pas rendre la fonction d'assistance (usine) encore meilleure qu'elle ne l'est en réalité.

Ce qui aurait été fait pour commencer:

function SomeConstructor(x, y, ...) { 
    // check if `this` is created with new 
    if (!(this instanceof arguments.callee)) 
     return new arguments.callee.apply (this, arguments); 
    // normal initialization code follows 

Un autre problème avec factory est qu'il va à l'encontre de la propriété de la longueur de la fonction.

9

Cela encourage simplement un raccourci mauvaise habitude qui repose beaucoup trop sur la mise en œuvre de la classe à « fixer » le code d'appel.

S'il s'agit d'un problème, ne le laissez pas glisser, ne lancez pas de message d'erreur.

+1

'new' introduit une irrégularité dans un code par ailleurs uniformément fonctionnel. ce n'est pas une valeur, si vous voulez la coller quelque part, vous devez l'inclure dans une expression 'function'. vous êtes de retour à la case départ, sauf que maintenant vous avez du code répétitif à chaque appel. mauvaise habitude? exemple stupide: 'map (new_ (Array), range (0, 10))' versus 'map (fonction (i) {return new Array (i);}, intervalle (0, 10))' –

-1

la seule chose qui a fonctionné pour moi consiste à réduire la mise en œuvre.il est laid, mais fonctionne (avec et sans opérateur nouveau):

var new_ = function (cls) 
{ 
    var constructors = [ 
     function() 
     { 
      return new cls(); 
     } 
     , function ($0) 
     { 
      return new cls($0); 
     } 
     , function ($0, $1) 
     { 
      return new cls($0, $1); 
     } 
     , function ($0, $1, $2) 
     { 
      return new cls($0, $1, $2); 
     } 
     , // up to a chosen limit 
    ]; 
    return function() 
    { 
     return constructors[arguments.length].apply(
      this 
      , arguments 
     ); 
    } 
} 

modifier réagir aux commentaires

je tolérance moyen-inférieur à la moyenne au code répétitif, et ce code de mal à écrire, mais les fonctions dans constructors doivent être séparées si arguments.length doit signifier quelque chose dans la fonction de constructeur réel. envisager une variante avec une seule enveloppe new:

var new_ = function (cls) 
{ 
    // arbitrary limit 
    var constructor = function ($0, $1, $2) 
    { 
     return new cls($0, $1, $2); 
    }; 
    return function() 
    { 
     return constructor.apply(
      this 
      , arguments 
     ); 
    } 
} 

var gen = new_(function() 
{ 
    print(
     arguments.length 
     + " " 
     + Array.prototype.toSource.call(arguments) 
    ); 
}); 
gen("foo")    // 3 ["foo", undefined, undefined] 
gen("foo", "bar")  // 3 ["foo", "bar", undefined] 
gen("foo", "bar", "baz") // 3 ["foo", "bar", "baz"] 

la liste des paramètres peut être arbitrairement large, mais arguments.length ne réside pas seulement dans le cas particulier.

J'ai utilisé cette solution avec la limite supérieure de 10 arguments pendant quelques années, et je ne me souviens pas d'avoir jamais atteint la limite. le risque que cela se produise est plutôt faible: tout le monde sait que les fonctions avec de nombreux paramètres sont un non-non, et javascript a une meilleure interface pour la fonctionnalité désirée: empaqueter les paramètres dans les objets. Ainsi, la seule limite est la largeur de la liste des paramètres, et cette limite semble purement théorique. à part ça, il supporte des constructeurs complètement arbitraires, donc je dirais que c'est très général.

+0

Puisqu'il est valide pour appelez les fonctions avec trop d'arguments, vous pourriez probablement juste vous en sortir avec le plus long appelant de taille fixe. Ou juste 'function applynew (cls, a) {return new cls (a [0], a [1], a [2], a [3], a [4]); } '? – bobince

+0

en utilisant seulement la forme la plus longue casserait 'arguments' dans' cls'.'applynew' ne résout pas le problème, et l'interface est moche. –

+0

-1: Pas général. –

3

Votre fonction « factory » pourrait être écrit ainsi:

function factory(fn, /* arg1, arg2, ..., argn */) { 
    var obj = new fn(), // Instantiate using 'new' to preserve the prototype chain 
     args = Array.prototype.slice.call(arguments, 1); // remove fn argument 
    fn.apply(obj, args); // apply the constructor again, with the right arguments 
    return obj; 
} 

// Test usage: 
function SomeConstructor (foo, bar) { 
    this.foo = foo; 
    this.bar = bar; 
} 
SomeConstructor.prototype.test = true; 

var o = factory(SomeConstructor, 'foo', 'bar'); 
// will return: Object foo=foo bar=bar test=true, and 
o instanceof SomeConstructor; // true 

Cependant, l'opérateur new est pas mal, je ne vous encourage à éviter, je vous recommande de rester avec un bon convention de nommage, identificateur de fonctions constructeur dans PascalCase, tous les autres identifiants dans camelCase, et aussi je vous recommande fortement d'utiliser JsLint il vous aidera à détecter ce genre d'erreurs au début.

+0

Ne fonctionnerait pas en usine (fn,/* arg1, arg2, ..., argn * /) {return nouveau fn (Array.prototype.slice.call (arguments, 1)); } est l'équivalent le plus court qui éviterait également les erreurs dans les constructeurs qui peuvent lancer des exceptions lorsqu'ils sont appelés sans arguments? En dehors de cela, je suis entièrement d'accord avec vos notes sur «nouveau» n'est pas mauvais. –

+0

@Justin: Votre exemple n'est pas équivalent, il exécutera la fonction constructeur en ne passant que l'argument ** un **, un objet Array. En utilisant la fonction 'SomeConstructor (foo, bar)' que j'ai posté en exemple, avec votre code, l'argument 'foo' contiendra' ['foo', 'bar'] 'et l'argument' bar' sera 'non défini ' . Le but de 'apply' est de passer les arguments séparés, comme attendu par la fonction. – CMS

+0

@Justin: BTW il y a quelques heures, j'ai répondu à une question plus en profondeur sur 'apply': http://stackoverflow.com/questions/1976015/how-to-implement-apply-pattern-in-javascript – CMS

0

Si vous êtes intéressé à faire face à l'impossibilité d'utiliser appliquer à nouveau, on pourrait utiliser

Function.prototype.New = (function() { 
    var fs = []; 
    return function() { 
    var f = fs [arguments.length]; 
    if (f) { 
     return f.apply (this, arguments); 
    } 
    var argStrs = []; 
    for (var i = 0; i < arguments.length; ++i) { 
     argStrs.push ("a[" + i + "]"); 
    } 
    f = new Function ("var a=arguments;return new this(" + argStrs.join() + ");"); 
    if (arguments.length < 100) { 
     fs [arguments.length] = f; 
    } 
    return f.apply (this, arguments); 
    }; 
})(); 

Exemple:

var foo = Foo.New.apply (null, argArray); 
+0

Pourquoi avez-vous répondu deux fois? –

+0

Parce que les réponses ne sont pas liées. –

1

tout « nouveau » est une bonne chose, et Je ne suis pas d'accord pour essayer de supprimer les fonctionnalités du langage, consultez ce code que j'ai joué il y a quelques temps: (notez que ce n'est pas une solution complète pour vous, mais plutôt quelque chose à intégrer dans votre code)

function proxy(obj) 
{ 
    var usingNew = true; 
    var obj_proxy = function() 
    { 
     if (usingNew) 
      this.constructor_new.apply(this, arguments); 
    }; 
    obj_proxy.prototype = obj.prototype; 
    obj_proxy.prototype.constructor_new = obj.prototype.constructor; 

    obj_proxy.createInstance = function() 
    { 
     usingNew = false; 
     var instance = new obj_proxy(); 
     instance.constructor_new.apply(instance, arguments); 
     usingNew = true; 

     return instance; 
    } 

    return obj_proxy; 
} 

pour le tester, créer une fonction foo comme ceci:

function foo(a, b) { this.a = a; } 

et le tester:

var foo1 = proxy(foo); 

var test1 = new foo1(1); 
alert(test1 instanceof foo); 

var test2 = foo1.createInstance(2); 
alert(test2 instanceof foo); 

EDIT: suppression du code non pertinent pour cela.

0

Voici un code de gril que j'ai trouvé comme modèle de code pour l'usine d'objets dans AngularjS. J'ai utilisé un Car/CarFactory comme exemple pour illustrer. Fait pour le code d'implémentation simple dans le contrôleur.

 <script> 
     angular.module('app', []) 
      .factory('CarFactory', function() { 

       /** 
       * BroilerPlate Object Instance Factory Definition/Example 
       */ 
       this.Car = function(color) { 

        // initialize instance properties 
        angular.extend(this, { 
         color   : null, 
         numberOfDoors : null, 
         hasFancyRadio : null, 
         hasLeatherSeats : null 
        }); 

        // generic setter (with optional default value) 
        this.set = function(key, value, defaultValue, allowUndefined) { 

         // by default, 
         if (typeof allowUndefined === 'undefined') { 
          // we don't allow setter to accept "undefined" as a value 
          allowUndefined = false; 
         } 
         // if we do not allow undefined values, and.. 
         if (!allowUndefined) { 
          // if an undefined value was passed in 
          if (value === undefined) { 
           // and a default value was specified 
           if (defaultValue !== undefined) { 
            // use the specified default value 
            value = defaultValue; 
           } else { 
            // otherwise use the class.prototype.defaults value 
            value = this.defaults[key]; 
           } // end if/else 
          } // end if 
         } // end if 

         // update 
         this[key] = value; 

         // return reference to this object (fluent) 
         return this; 

        }; // end this.set() 

       }; // end this.Car class definition 

       // instance properties default values 
       this.Car.prototype.defaults = { 
        color: 'yellow', 
        numberOfDoors: 2, 
        hasLeatherSeats: null 
       }; 

       // instance factory method/constructor 
       this.Car.prototype.instance = function(params) { 
        return new 
         this.constructor() 
           .set('color',   params.color) 
           .set('numberOfDoors', params.numberOfDoors) 
           .set('hasFancyRadio', params.hasFancyRadio) 
           .set('hasLeatherSeats', params.hasLeatherSeats) 
        ; 
       }; 

       return new this.Car(); 

      }) // end Factory Definition 
      .controller('testCtrl', function($scope, CarFactory) { 

       window.testCtrl = $scope; 

       // first car, is red, uses class default for: 
       // numberOfDoors, and hasLeatherSeats 
       $scope.car1  = CarFactory 
            .instance({ 
             color: 'red' 
            }) 
           ; 

       // second car, is blue, has 3 doors, 
       // uses class default for hasLeatherSeats 
       $scope.car2  = CarFactory 
            .instance({ 
             color: 'blue', 
             numberOfDoors: 3 
            }) 
           ; 
       // third car, has 4 doors, uses class default for 
       // color and hasLeatherSeats 
       $scope.car3  = CarFactory 
            .instance({ 
             numberOfDoors: 4 
            }) 
           ; 
       // sets an undefined variable for 'hasFancyRadio', 
       // explicitly defines "true" as default when value is undefined 
       $scope.hasFancyRadio = undefined; 
       $scope.car3.set('hasFancyRadio', $scope.hasFancyRadio, true); 

       // fourth car, purple, 4 doors, 
       // uses class default for hasLeatherSeats 
       $scope.car4  = CarFactory 
            .instance({ 
             color: 'purple' 
             numberOfDoors: 4 
            }); 
       // and then explicitly sets hasLeatherSeats to undefined 
       $scope.hasLeatherSeats = undefined; 
       $scope.car4.hasLeatherSeats.set('hasLeatherSeats', $scope.hasLeatherSeats, undefined, true); 

       // in console, type window.testCtrl to see the resulting objects 

      }); 
    </script> 
Questions connexes