Il existe deux modèles pour implémenter des classes et des instances en JavaScript: la méthode de prototypage et la méthode de fermeture. Les deux ont des avantages et des inconvénients, et il existe de nombreuses variantes étendues. De nombreux programmeurs et bibliothèques ont des approches et des fonctions utilitaires de gestion de classe différentes pour imprimer sur certaines des parties les plus laides du langage.
Le résultat est qu'en compagnie mixte, vous aurez un méli-mélo de métaclasses, toutes se comportant légèrement différemment. Pire encore, la plupart des didacticiels JavaScript sont terribles et servent de compromis intermédiaires pour couvrir toutes les bases, vous laissant très confus. (L'auteur est probablement aussi confus. Le modèle d'objet de JavaScript est très différent de la plupart des langages de programmation, et dans de nombreux endroits, il est mal conçu.)
Commençons par la manière prototype . C'est le plus natif de JavaScript que vous pouvez obtenir: il y a un minimum de code de surcharge et instanceof fonctionnera avec les instances de ce type d'objet.
function Shape(x, y) {
this.x= x;
this.y= y;
}
Nous pouvons ajouter des méthodes à l'instance créée en les new Shape
écrivant dans la prototype
recherche de cette fonction constructeur:
Shape.prototype.toString= function() {
return 'Shape at '+this.x+', '+this.y;
};
Maintenant, pour le sous-classer, dans la mesure où vous pouvez appeler ce que JavaScript fait de la sous-classe. Nous le faisons en remplaçant complètement cette prototype
propriété magique étrange :
function Circle(x, y, r) {
Shape.call(this, x, y); // invoke the base class's constructor function to take co-ords
this.r= r;
}
Circle.prototype= new Shape();
avant d'y ajouter des méthodes:
Circle.prototype.toString= function() {
return 'Circular '+Shape.prototype.toString.call(this)+' with radius '+this.r;
}
Cet exemple fonctionnera et vous verrez du code similaire dans de nombreux didacticiels. Mais l'homme, c'est new Shape()
moche: nous instancions la classe de base même si aucune forme réelle n'est à créer. Il se trouve que cela fonctionne dans ce cas simple parce que JavaScript est tellement bâclé: il permet de passer zéro arguments, auquel cas x
et y
devenir undefined
et sont assignés aux prototypes this.x
et this.y
. Si la fonction constructeur faisait quelque chose de plus compliqué, elle tomberait à plat sur son visage.
Donc, ce que nous devons faire, c'est trouver un moyen de créer un objet prototype qui contient les méthodes et autres membres que nous voulons au niveau de la classe, sans appeler la fonction constructeur de la classe de base. Pour ce faire, nous allons devoir commencer à écrire du code d'aide. C'est l'approche la plus simple que je connaisse:
function subclassOf(base) {
_subclassOf.prototype= base.prototype;
return new _subclassOf();
}
function _subclassOf() {};
Cela transfère les membres de la classe de base dans son prototype vers une nouvelle fonction constructeur qui ne fait rien, puis utilise ce constructeur. Maintenant, nous pouvons écrire simplement:
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.prototype= subclassOf(Shape);
à la place du new Shape()
injustice. Nous avons maintenant un ensemble acceptable de primitives pour les classes construites.
Il y a quelques améliorations et extensions que nous pouvons considérer sous ce modèle. Par exemple, voici une version syntaxique de sucre:
Function.prototype.subclass= function(base) {
var c= Function.prototype.subclass.nonconstructor;
c.prototype= base.prototype;
this.prototype= new c();
};
Function.prototype.subclass.nonconstructor= function() {};
...
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.subclass(Shape);
L'une ou l'autre version présente l'inconvénient que la fonction constructeur ne peut pas être héritée, comme c'est le cas dans de nombreux langages. Ainsi, même si votre sous-classe n'ajoute rien au processus de construction, elle doit se rappeler d'appeler le constructeur de base avec les arguments souhaités par la base. Cela peut être légèrement automatisé en utilisant apply
, mais vous devez toujours écrire:
function Point() {
Shape.apply(this, arguments);
}
Point.subclass(Shape);
Ainsi, une extension courante consiste à décomposer les éléments d'initialisation en sa propre fonction plutôt que le constructeur lui-même. Cette fonction peut alors très bien hériter de la base:
function Shape() { this._init.apply(this, arguments); }
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
function Point() { this._init.apply(this, arguments); }
Point.subclass(Shape);
// no need to write new initialiser for Point!
Nous avons maintenant le même passe-partout de fonction constructeur pour chaque classe. Peut-être pouvons-nous déplacer cela dans sa propre fonction d'assistance afin que nous n'ayons pas à le taper, par exemple au lieu de le Function.prototype.subclass
retourner et laisser la fonction Function de la classe de base cracher des sous-classes:
Function.prototype.makeSubclass= function() {
function Class() {
if ('_init' in this)
this._init.apply(this, arguments);
}
Function.prototype.makeSubclass.nonconstructor.prototype= this.prototype;
Class.prototype= new Function.prototype.makeSubclass.nonconstructor();
return Class;
};
Function.prototype.makeSubclass.nonconstructor= function() {};
...
Shape= Object.makeSubclass();
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
Point= Shape.makeSubclass();
Circle= Shape.makeSubclass();
Circle.prototype._init= function(x, y, r) {
Shape.prototype._init.call(this, x, y);
this.r= r;
};
... qui commence à ressembler un peu plus à d'autres langages, mais avec une syntaxe légèrement plus maladroite. Vous pouvez saupoudrer quelques fonctionnalités supplémentaires si vous le souhaitez. Vous souhaitez peut-être makeSubclass
prendre et mémoriser un nom de classe et fournir une valeur toString
par défaut en l' utilisant. Peut-être que vous voulez que le constructeur détecte quand il a été accidentellement appelé sans l' new
opérateur (ce qui entraînerait souvent un débogage très ennuyeux):
Function.prototype.makeSubclass= function() {
function Class() {
if (!(this instanceof Class))
throw('Constructor called without "new"');
...
Peut-être que vous voulez passer tous les nouveaux membres et les makeSubclass
ajouter au prototype, pour vous éviter d'avoir à écrire Class.prototype...
beaucoup. De nombreux systèmes de classe le font, par exemple:
Circle= Shape.makeSubclass({
_init: function(x, y, z) {
Shape.prototype._init.call(this, x, y);
this.r= r;
},
...
});
Il existe de nombreuses fonctionnalités potentielles que vous pourriez considérer comme souhaitables dans un système d'objets et personne n'est vraiment d'accord sur une formule particulière.
La fermeture , alors. Cela évite les problèmes d'héritage basé sur un prototype de JavaScript, en n'utilisant pas du tout l'héritage. Au lieu:
function Shape(x, y) {
var that= this;
this.x= x;
this.y= y;
this.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
}
function Circle(x, y, r) {
var that= this;
Shape.call(this, x, y);
this.r= r;
var _baseToString= this.toString;
this.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+that.r;
};
};
var mycircle= new Circle();
Désormais, chaque instance de Shape
aura sa propre copie du fichiertoString
méthode (et toute autre méthode ou tout autre membre de classe que nous ajouterons).
La mauvaise chose à propos de chaque instance ayant sa propre copie de chaque membre de la classe est qu'elle est moins efficace. Si vous avez affaire à un grand nombre d'instances sous-classées, l'héritage prototypique peut mieux vous servir. Appeler également une méthode de la classe de base est un peu ennuyeux comme vous pouvez le voir: nous devons nous souvenir de ce qu'était la méthode avant que le constructeur de la sous-classe ne l'écrase, sinon elle se perd.
[Aussi parce qu'il n'y a pas d'héritage ici, l' instanceof
opérateur ne fonctionnera pas; vous devrez fournir votre propre mécanisme de reniflement de classe si vous en avez besoin. Bien que vous puissiez manipuler les objets prototypes de la même manière qu'avec l'héritage de prototypes, c'est un peu délicat et cela ne vaut pas vraiment la peine de se mettre au instanceof
travail.]
La bonne chose à propos de chaque instance ayant sa propre méthode est que la méthode peut alors être liée à l'instance spécifique qui la possède. Ceci est utile en raison de la façon étrange de liaison de JavaScript this
dans les appels de méthode, qui a le résultat que si vous détachez une méthode de son propriétaire:
var ts= mycircle.toString;
alert(ts());
puis this
à l'intérieur de la méthode ne sera pas l'instance Circle comme prévu (ce sera en fait l' window
objet global , provoquant un malheur de débogage généralisé). En réalité, cela se produit généralement lorsqu'une méthode est prise et affectée à un setTimeout
, onclick
ouEventListener
en général.
Avec la méthode prototype, vous devez inclure une fermeture pour chaque mission de ce type:
setTimeout(function() {
mycircle.move(1, 1);
}, 1000);
ou, à l'avenir (ou maintenant si vous piratez Function.prototype), vous pouvez également le faire avec function.bind()
:
setTimeout(mycircle.move.bind(mycircle, 1, 1), 1000);
si vos instances sont effectuées de la manière de fermeture, la liaison est effectuée gratuitement par la fermeture sur la variable d'instance (généralement appelée that
ou self
, bien que personnellement je déconseille cette dernière car elle a self
déjà une autre signification différente en JavaScript). 1, 1
Cependant, vous ne recevez pas les arguments dans l'extrait ci-dessus gratuitement, vous aurez donc toujours besoin d'une autre fermeture ou d'un bind()
si vous avez besoin de le faire.
Il existe également de nombreuses variantes de la méthode de fermeture. Vous pouvez préférer omettre this
complètement, en créer un nouveau that
et le renvoyer au lieu d'utiliser l' new
opérateur:
function Shape(x, y) {
var that= {};
that.x= x;
that.y= y;
that.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
return that;
}
function Circle(x, y, r) {
var that= Shape(x, y);
that.r= r;
var _baseToString= that.toString;
that.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+r;
};
return that;
};
var mycircle= Circle(); // you can include `new` if you want but it won't do anything
Quelle voie est «appropriée»? Tous les deux. Quel est le «meilleur»? Cela dépend de votre situation. FWIW J'ai tendance à prototyper pour un véritable héritage JavaScript lorsque je fais fortement des trucs OO, et des fermetures pour de simples effets de page jetables.
Mais les deux façons sont assez contre-intuitives pour la plupart des programmeurs. Les deux ont de nombreuses variations potentiellement désordonnées. Vous rencontrerez les deux (ainsi que de nombreux schémas intermédiaires et généralement cassés) si vous utilisez le code / les bibliothèques d'autres personnes. Il n'y a pas de réponse généralement acceptée. Bienvenue dans le monde merveilleux des objets JavaScript.
[Cela fait partie 94 de Pourquoi JavaScript n'est pas mon langage de programmation préféré.]