Hay dos modelos para implementar clases e instancias en JavaScript: la forma de creación de prototipos y la forma de cierre. Ambos tienen ventajas y desventajas, y hay muchas variaciones extendidas. Muchos programadores y bibliotecas tienen diferentes enfoques y funciones de utilidad de manejo de clases para empapelar algunas de las partes más feas del lenguaje.
El resultado es que en una empresa mixta tendrá una mezcla de metaclases, todas con un comportamiento ligeramente diferente. Lo que es peor, la mayoría del material de tutorial de JavaScript es terrible y ofrece algún tipo de compromiso intermedio para cubrir todas las bases, dejándolo muy confundido. (Probablemente, el autor también está confundido. El modelo de objetos de JavaScript es muy diferente a la mayoría de los lenguajes de programación y, en muchos lugares, está mal diseñado).
Comencemos con el prototipo . Esta es la versión más nativa de JavaScript que puede obtener: hay un mínimo de código indirecto y instancia de funcionará con instancias de este tipo de objeto.
function Shape(x, y) {
this.x= x;
this.y= y;
}
Podemos agregar métodos a la instancia creada new Shapeescribiéndolos en la prototypebúsqueda de esta función constructora:
Shape.prototype.toString= function() {
return 'Shape at '+this.x+', '+this.y;
};
Ahora para subclasificarlo, en la medida en que puede llamar lo que JavaScript hace subclases. Hacemos eso reemplazando por completo esa extraña prototypepropiedad mágica :
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();
antes de agregarle métodos:
Circle.prototype.toString= function() {
return 'Circular '+Shape.prototype.toString.call(this)+' with radius '+this.r;
}
Este ejemplo funcionará y verá un código similar en muchos tutoriales. Pero hombre, eso new Shape()es feo: estamos creando instancias de la clase base a pesar de que no se va a crear una Forma real. Le pasa a trabajar en este caso sencillo, porque JavaScript está tan descuidado: permite cero argumentos que se pasan en, en cuyo caso xy yse vuelven undefinedy se asignan al prototipo de this.xy this.y. Si la función constructora estuviera haciendo algo más complicado, se caería de bruces.
Entonces, lo que debemos hacer es encontrar una forma de crear un objeto prototipo que contenga los métodos y otros miembros que queremos a nivel de clase, sin llamar a la función constructora de la clase base. Para hacer esto, vamos a tener que comenzar a escribir código auxiliar. Este es el enfoque más simple que conozco:
function subclassOf(base) {
_subclassOf.prototype= base.prototype;
return new _subclassOf();
}
function _subclassOf() {};
Esto transfiere los miembros de la clase base en su prototipo a una nueva función de constructor que no hace nada, luego usa ese constructor. Ahora podemos escribir simplemente:
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.prototype= subclassOf(Shape);
en lugar de lo new Shape()incorrecto. Ahora tenemos un conjunto aceptable de primitivas para clases construidas.
Hay algunos refinamientos y extensiones que podemos considerar bajo este modelo. Por ejemplo, aquí hay una versión sintáctica de azúcar:
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);
Cualquiera de las versiones tiene el inconveniente de que la función de constructor no se puede heredar, como sucede en muchos idiomas. Entonces, incluso si su subclase no agrega nada al proceso de construcción, debe recordar llamar al constructor base con cualquier argumento que la base desee. Esto se puede automatizar un poco apply, pero aún debe escribir:
function Point() {
Shape.apply(this, arguments);
}
Point.subclass(Shape);
Entonces, una extensión común es dividir el material de inicialización en su propia función en lugar del propio constructor. Esta función puede heredar de la base muy bien:
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!
Ahora acabamos de obtener la misma plantilla de función de constructor para cada clase. Tal vez podamos moverlo a su propia función auxiliar para que no tengamos que seguir tipeándolo, por ejemplo, en lugar de Function.prototype.subclassgirarlo y dejar que la función de la clase base escupe subclases:
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;
};
... que está empezando a parecerse un poco más a otros idiomas, aunque con una sintaxis un poco más torpe. Puede agregar algunas características adicionales si lo desea. Tal vez desee makeSubclasstomar y recordar un nombre de clase y proporcionar un valor predeterminado para toStringusarlo. Tal vez desee hacer que el constructor detecte cuándo se ha llamado accidentalmente sin el newoperador (que de lo contrario a menudo resultaría en una depuración muy molesta):
Function.prototype.makeSubclass= function() {
function Class() {
if (!(this instanceof Class))
throw('Constructor called without "new"');
...
Tal vez desee pasar a todos los nuevos miembros y makeSubclassagregarlos al prototipo, para evitar tener que escribir Class.prototype...tanto. Muchos sistemas de clase hacen eso, por ejemplo:
Circle= Shape.makeSubclass({
_init: function(x, y, z) {
Shape.prototype._init.call(this, x, y);
this.r= r;
},
...
});
Hay muchas características potenciales que podría considerar deseables en un sistema de objetos y nadie está realmente de acuerdo con una fórmula en particular.
La forma de cierre , entonces. Esto evita los problemas de la herencia basada en prototipos de JavaScript, al no utilizar la herencia en absoluto. En lugar:
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();
Ahora cada instancia de Shapetendrá su propia copia del toStringmétodo (y cualquier otro método u otro miembro de clase que agreguemos).
Lo malo de que cada instancia tenga su propia copia de cada miembro de la clase es que es menos eficiente. Si se trata de un gran número de instancias subclasificadas, la herencia prototípica puede servirle mejor. Además, llamar a un método de la clase base es un poco molesto como puede ver: tenemos que recordar cuál era el método antes de que el constructor de la subclase lo sobrescribiera, o se pierde.
[También porque no hay herencia aquí, el instanceofoperador no funcionará; Tendría que proporcionar su propio mecanismo para detectar clases si lo necesita. Si bien podría manipular los objetos prototipo de una manera similar a la de la herencia prototipo, es un poco complicado y realmente no vale la pena simplemente comenzar a instanceoftrabajar.]
Lo bueno de que cada instancia tenga su propio método es que el método puede estar vinculado a la instancia específica que lo posee. Esto es útil debido a la extraña forma de vinculación de JavaScript thisen las llamadas a métodos, que tiene el resultado de que si desconecta un método de su propietario:
var ts= mycircle.toString;
alert(ts());
luego, thisdentro del método no estará la instancia de Circle como se esperaba (en realidad será el windowobjeto global , causando problemas de depuración generalizados). En realidad, esto suele ocurrir cuando se toma un método y se le asigna a setTimeout, onclicko EventListeneren general.
Con el prototipo, debe incluir un cierre para cada tarea:
setTimeout(function() {
mycircle.move(1, 1);
}, 1000);
o, en el futuro (o ahora si piratea Function.prototype) también puede hacerlo con function.bind():
setTimeout(mycircle.move.bind(mycircle, 1, 1), 1000);
si sus instancias se realizan de la manera de cierre, el enlace se realiza de forma gratuita mediante el cierre sobre la variable de instancia (generalmente se llama thato self, aunque personalmente recomendaría que esta última selfya tenga otro significado diferente en JavaScript). Sin 1, 1embargo, no obtienes los argumentos en el fragmento anterior de forma gratuita, por lo que aún necesitarías otro cierre o un a bind()si necesitas hacerlo.
También hay muchas variantes en el método de cierre. Es posible que prefiera omitir por thiscompleto, crear uno nuevo thaty devolverlo en lugar de usar el newoperador:
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
¿Qué camino es "correcto"? Ambos. Cuál es el mejor"? Eso depende de tu situación. FWIW Tiendo a la creación de prototipos para la herencia real de JavaScript cuando estoy haciendo mucho OO y cierres para efectos de página desechables simples.
Pero ambas formas son bastante contra intuitivas para la mayoría de los programadores. Ambos tienen muchas posibles variaciones desordenadas. Conocerá ambos (así como muchos esquemas intermedios y generalmente rotos) si utiliza el código / bibliotecas de otras personas. No hay una respuesta generalmente aceptada. Bienvenido al maravilloso mundo de los objetos JavaScript.
[Esto ha sido parte 94 de Por qué JavaScript no es mi lenguaje de programación favorito.]