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 Shape
escribiéndolos en la prototype
bú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 prototype
propiedad 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 x
y y
se vuelven undefined
y se asignan al prototipo de this.x
y 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.subclass
girarlo 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 makeSubclass
tomar y recordar un nombre de clase y proporcionar un valor predeterminado para toString
usarlo. Tal vez desee hacer que el constructor detecte cuándo se ha llamado accidentalmente sin el new
operador (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 makeSubclass
agregarlos 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 Shape
tendrá su propia copia del toString
mé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 instanceof
operador 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 instanceof
trabajar.]
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 this
en 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, this
dentro del método no estará la instancia de Circle como se esperaba (en realidad será el window
objeto global , causando problemas de depuración generalizados). En realidad, esto suele ocurrir cuando se toma un método y se le asigna a setTimeout
, onclick
o EventListener
en 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 that
o self
, aunque personalmente recomendaría que esta última self
ya tenga otro significado diferente en JavaScript). Sin 1, 1
embargo, 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 this
completo, crear uno nuevo that
y devolverlo en lugar de usar el new
operador:
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.]