Éviter un nouvel opérateur en JavaScript - la meilleure façon

16

Attention: c'est un long post.

Restons simples. Je veux éviter d'avoir à préfixer le nouvel opérateur chaque fois que j'appelle un constructeur en JavaScript. C'est parce que j'ai tendance à l'oublier et que mon code se fout mal.

La façon simple de contourner cela est la suivante ...

function Make(x) {
  if ( !(this instanceof arguments.callee) )
  return new arguments.callee(x);

  // do your stuff...
}

Mais, j'en ai besoin pour accepter la variable no. d'arguments, comme ça ...

m1 = Make();
m2 = Make(1,2,3);
m3 = Make('apple', 'banana');

La première solution immédiate semble être la méthode «appliquer» comme celle-ci ...

function Make() {
  if ( !(this instanceof arguments.callee) )
    return new arguments.callee.apply(null, arguments);

  // do your stuff
}

C'est faux cependant - le nouvel objet est passé à la applyméthode et NON à notre constructeur arguments.callee.

Maintenant, j'ai trouvé trois solutions. Ma question simple est: laquelle semble la meilleure. Ou, si vous avez une meilleure méthode, dites-le.

Premier - utilisez eval()pour créer dynamiquement du code JavaScript qui appelle le constructeur.

function Make(/* ... */) {
  if ( !(this instanceof arguments.callee) ) {
    // collect all the arguments
    var arr = [];
    for ( var i = 0; arguments[i]; i++ )
      arr.push( 'arguments[' + i + ']' );

    // create code
    var code = 'new arguments.callee(' + arr.join(',') + ');';

    // call it
    return eval( code );
  }

  // do your stuff with variable arguments...
}

Deuxièmement - Chaque objet a une __proto__propriété qui est un lien «secret» vers son objet prototype. Heureusement, cette propriété est accessible en écriture.

function Make(/* ... */) {
  var obj = {};

  // do your stuff on 'obj' just like you'd do on 'this'
  // use the variable arguments here

  // now do the __proto__ magic
  // by 'mutating' obj to make it a different object

  obj.__proto__ = arguments.callee.prototype;

  // must return obj
  return obj;
}

Troisième - C'est quelque chose de similaire à la deuxième solution.

function Make(/* ... */) {
  // we'll set '_construct' outside
  var obj = new arguments.callee._construct();

  // now do your stuff on 'obj' just like you'd do on 'this'
  // use the variable arguments here

  // you have to return obj
  return obj;
}

// now first set the _construct property to an empty function
Make._construct = function() {};

// and then mutate the prototype of _construct
Make._construct.prototype = Make.prototype;

  • eval la solution semble maladroite et s'accompagne de tous les problèmes de "evil eval".

  • __proto__ La solution n'est pas standard et le «grand navigateur de mIsERY» ne l'honore pas.

  • La troisième solution semble trop compliquée.

Mais avec les trois solutions ci-dessus, nous pouvons faire quelque chose comme ça, que nous ne pouvons pas autrement ...

m1 = Make();
m2 = Make(1,2,3);
m3 = Make('apple', 'banana');

m1 instanceof Make; // true
m2 instanceof Make; // true
m3 instanceof Make; // true

Make.prototype.fire = function() {
  // ...
};

m1.fire();
m2.fire();
m3.fire();

Donc, effectivement, les solutions ci-dessus nous donnent de "vrais" constructeurs qui acceptent la variable no. d'arguments et ne nécessitent pas new. Quel est votre avis là-dessus.

-- MISE À JOUR --

Certains ont dit "jette juste une erreur". Ma réponse est: nous faisons une application lourde avec plus de 10 constructeurs et je pense que ce serait beaucoup plus maniable si chaque constructeur pouvait gérer "intelligemment" cette erreur sans lancer de messages d'erreur sur la console.

treecoder
la source
2
ou jetez simplement une erreur quand elle est mauvaise examinez la trace de pile et vous pouvez corriger le code
ratchet freak
2
Je pense que cette question serait mieux posée sur Stack Overflow ou Code Review . Cela semble être plutôt centré sur le code plutôt qu'une question conceptuelle.
Adam Lear
1
@greengit plutôt que de lancer une erreur, utilisez un jslint. Il vous avertit si vous avez fait Make()sans newraison Make est capitalisé et donc il suppose qu'il est un constructeur
Raynos
1
Alors attendez - cherchez-vous une meilleure façon d'accomplir cela, ou cherchez-vous simplement quelqu'un pour vous donner du code afin que vous puissiez avoir la création d'objets à arguments variables sans new? Parce que si c'est le dernier, vous demandez probablement sur le mauvais site. Si c'est le premier, vous voudrez peut-être ne pas rejeter les suggestions concernant l'utilisation de nouvelles erreurs et la détection si rapide ... Si votre application est vraiment "lourde", la dernière chose que vous voulez est un mécanisme de construction surchargé pour la ralentir. new, malgré tout le flack qu'il obtient, est assez rapide.
Shog9
5
Ironiquement, essayer de gérer «intelligemment» les erreurs de programmation est lui-même responsable de la plupart des «mauvaises parties» de JavaScript.
Daniel Pratt

Réponses:

19

Tout d'abord arguments.calleeest obsolète dans ES5 strict, nous ne l'utilisons donc pas. La vraie solution est plutôt simple.

Vous n'utilisez pas newdu tout.

var Make = function () {
  if (Object.getPrototypeOf(this) !== Make.prototype) {
    var o = Object.create(Make.prototype);
    o.constructor.apply(o, arguments);
    return o;
  }
  ...
}

C'est une bonne douleur dans le cul non?

Essayer enhance

var Make = enhance(function () {
  ...
});

var enhance = function (constr) {
  return function () {
    if (Object.getPrototypeOf(this) !== constr.prototype) {
      var o = Object.create(constr.prototype);
      constr.apply(o, arguments);
      return o;
    }
    return constr.apply(this, arguments);
  }
}

Maintenant, bien sûr, cela nécessite ES5, mais tout le monde utilise la cale ES5, n'est -ce pas?

Vous pouvez également être intéressé par des modèles alternatifs js OO

En passant, vous pouvez remplacer l'option deux par

var Make = function () {
  var that = Object.create(Make.prototype);
  // use that 

  return that;
}

Si vous voulez votre propre Object.createcale ES5, c'est très simple

Object.create = function (proto) {
  var dummy = function () {};
  dummy.prototype = proto;
  return new dummy;
};
Raynos
la source
Oui vrai - mais toute la magie ici est à cause de Object.create. Et avant ES5? ES5-Shim est répertorié Object.createcomme DUBIOUS.
treecoder
@greengit si vous le relisez, le shim ES5 indique que le deuxième argument d'Object.create est DUBIOUS. le premier est très bien.
Raynos
1
Oui, je l'ai lu. Et je pense (IF) qu'ils utilisent une sorte de __proto__chose là-bas, alors nous sommes toujours sur le même point. Parce qu'avant ES5, il n'y a PAS de moyen plus simple de muter le prototype. Mais de toute façon, votre solution semble la plus élégante et tournée vers l'avenir. +1 pour cela. (ma limite de vote est atteinte)
Treecoder
1
Merci @psr. Et @Raynos, votre Object.createcale est à peu près ma troisième solution mais moins compliquée et plus belle que la mienne bien sûr.
treecoder
37

Restons simples. Je veux éviter d'avoir à préfixer le nouvel opérateur chaque fois que j'appelle un constructeur en JavaScript. C'est parce que j'ai tendance à l'oublier et que mon code se fout mal.

La réponse évidente serait de ne pas oublier le newmot - clé.

Vous changez la structure et le sens de la langue.

Ce qui, à mon avis, et pour le bien des futurs responsables de votre code, est une idée horrible.

CaffGeek
la source
9
+1 Il semble étrange de débattre un langage autour de ses mauvaises habitudes de codage. Certes, une politique de mise en évidence / d'enregistrement de la syntaxe peut permettre d'éviter les modèles sujets aux bogues / les fautes de frappe probables.
Stoive
Si je trouvais une bibliothèque où tous les constructeurs se moquaient de savoir si vous l'utilisiez newou non, je trouverais celle-ci plus maintenable.
Jack
@Jack, mais cela introduira beaucoup plus de difficultés et de subtilité pour trouver les bogues. Il suffit de regarder tous les bugs causés par le javascript "ne se souciant pas" si vous incluez les ;instructions to end. (Insertion automatique des points-virgules)
CaffGeek
@CaffGeek "Javascript ne se soucie pas des points-virgules" n'est pas exact - il y a des cas où l'utilisation d'un point-virgule et sa non-utilisation sont subtilement différentes, et il y a des cas où ils ne le sont pas. C'est le problème. La solution présentée dans la réponse acceptée est précisément le contraire - avec elle, dans tous les cas, l' utilisation newou non est sémantiquement identique . Il n'y a aucun cas subtil où cet invariant est rompu. C'est pourquoi c'est bon et pourquoi vous voudriez l'utiliser.
Jack
14

La solution la plus simple consiste à se souvenir newet à lancer une erreur pour rendre évident que vous avez oublié.

if (Object.getPrototypeOf(this) !== Make.prototype) {
    throw new Error('Remember to call "new"!');
}

Quoi que vous fassiez, ne l'utilisez pas eval. Je n'hésiterais pas à utiliser des propriétés non standard, __proto__notamment parce qu'elles ne le sont pas et que leurs fonctionnalités peuvent changer.

Josh K
la source
Défiez-le, .__proto__c'est le diable
Raynos
3

J'ai en fait écrit un article à ce sujet. http://js-bits.blogspot.com/2010/08/constructors-without-using-new.html

function Ctor() {
    if (!(this instanceof Ctor) {
        return new Ctor(); 
    }
    // regular construction code
}

Et vous pouvez même le généraliser pour ne pas avoir à ajouter cela en haut de chaque constructeur. Vous pouvez le voir en visitant mon post

Clause de non- responsabilité Je ne l'utilise pas dans mon code, je ne l'ai publié que pour la valeur didactique. J'ai trouvé qu'oublier un newest un bug facile à repérer. Comme d'autres, je ne pense pas que nous en ayons réellement besoin pour la plupart des codes. Sauf si vous écrivez une bibliothèque pour créer l'héritage JS, auquel cas vous pouvez utiliser à partir d'un seul endroit et vous utiliseriez déjà une approche différente de l'héritage direct.

Juan Mendes
la source
Bug potentiellement caché: si j'ai var x = new Ctor();et puis plus tard j'ai x as thiset do var y = Ctor();, cela ne se comporterait pas comme prévu.
luiscubal
@luiscubal Vous ne savez pas ce que vous dites "plus tard, x as this", pouvez-vous peut-être poster un jsfiddle pour montrer le problème potentiel?
Juan Mendes
Votre code est un peu plus robuste que je ne le pensais au départ, mais j'ai réussi à trouver un exemple (quelque peu compliqué mais toujours valide): jsfiddle.net/JHNcR/1
luiscubal
@luiscubal Je vois votre point mais c'est vraiment compliqué. Vous supposez que c'est OK d'appeler Ctor.call(ctorInstance, 'value'). Je ne vois pas de scénario valable pour ce que vous faites. Pour construire un objet, vous utilisez soit var x = new Ctor(val)ou var y=Ctor(val). Même s'il y avait un scénario valide, je prétends que vous pouvez avoir des constructeurs sans utiliser new Ctor, pas que vous pouvez avoir des constructeurs qui fonctionnent en utilisant Ctor.callVoir jsfiddle.net/JHNcR/2
Juan Mendes
0

Que dis-tu de ça?

/* thing maker! it makes things! */
function thing(){
    if (!(this instanceof thing)){
        /* call 'new' for the lazy dev who didn't */
        return new thing(arguments, "lazy");
    };

    /* figure out how to use the arguments object, based on the above 'lazy' flag */
    var args = (arguments.length > 0 && arguments[arguments.length - 1] === "lazy") ? arguments[0] : arguments;

    /* set properties as needed */
    this.prop1 = (args.length > 0) ? args[0] : "nope";
    this.prop2 = (args.length > 1) ? args[1] : "nope";
};

/* create 3 new things (mixed 'new' and 'lazy') */
var myThing1 = thing("woo", "hoo");
var myThing2 = new thing("foo", "bar");
var myThing3 = thing();

/* test your new things */
console.log(myThing1.prop1); /* outputs 'woo' */
console.log(myThing1.prop2); /* outputs 'hoo' */

console.log(myThing2.prop1); /* outputs 'foo' */
console.log(myThing2.prop2); /* outputs 'bar' */

console.log(myThing3.prop1); /* outputs 'nope' */
console.log(myThing3.prop2); /* outputs 'nope' */

EDIT: j'ai oublié d'ajouter:

"Si votre application est vraiment" lourde ", la dernière chose que vous voulez, c'est un mécanisme de construction surchargé pour la ralentir"

Je suis absolument d'accord - lors de la création de «chose» ci-dessus sans le mot-clé «nouveau», il est plus lent / plus lourd qu'avec lui. Les erreurs sont votre ami, car elles vous disent ce qui ne va pas. De plus, ils disent à vos collègues développeurs ce qu'ils font de mal.

encodeur
la source
Inventer des valeurs pour les arguments de constructeur manquants est sujet aux erreurs. S'ils manquent, laissez les propriétés non définies. S'ils sont essentiels, lancez une erreur s'ils manquent. Et si prop1 était censé être booléen? Tester «non» pour la véracité va être une source d'erreurs.
JBRWilkinson