Esistono due modelli per l'implementazione di classi ed istanze in JavaScript: il modo di prototipazione e il modo di chiusura. Entrambi hanno vantaggi e svantaggi e ci sono molte varianti estese. Molti programmatori e librerie hanno approcci diversi e funzioni di utilità di gestione delle classi per scrivere su alcune delle parti più brutte del linguaggio.
Il risultato è che in società miste avrai un miscuglio di metaclassi, che si comportano tutti in modo leggermente diverso. Quel che è peggio, la maggior parte del materiale tutorial di JavaScript è terribile e offre una sorta di compromesso intermedio per coprire tutte le basi, lasciandoti molto confuso. (Probabilmente anche l'autore è confuso. Il modello a oggetti di JavaScript è molto diverso dalla maggior parte dei linguaggi di programmazione, e in molti punti è stato progettato male.)
Cominciamo con il prototipo . Questo è il più nativo di JavaScript che puoi ottenere: c'è un minimo di codice ambientale e instanceof funzionerà con istanze di questo tipo di oggetto.
function Shape(x, y) {
this.x= x;
this.y= y;
}
Possiamo aggiungere metodi all'istanza creata new Shape
scrivendoli nella prototype
ricerca di questa funzione di costruzione:
Shape.prototype.toString= function() {
return 'Shape at '+this.x+', '+this.y;
};
Ora per sottoclassarlo, in quanto è possibile chiamare ciò che JavaScript esegue la sottoclasse. Lo facciamo sostituendo completamente quella strana prototype
proprietà magica :
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();
prima di aggiungere metodi ad esso:
Circle.prototype.toString= function() {
return 'Circular '+Shape.prototype.toString.call(this)+' with radius '+this.r;
}
Questo esempio funzionerà e vedrai il codice simile in molti tutorial. Ma amico, new Shape()
è brutto: stiamo istanziando la classe base anche se non si deve creare una forma reale. Succede in questo semplice caso perché JavaScript è così sciatto: consente di passare zero argomenti, nel qual caso x
e y
diventare undefined
e sono assegnati al prototipo this.x
e this.y
. Se la funzione di costruzione stesse facendo qualcosa di più complicato, sarebbe caduta piatta sulla sua faccia.
Quindi quello che dobbiamo fare è trovare un modo per creare un oggetto prototipo che contenga i metodi e gli altri membri che desideriamo a livello di classe, senza chiamare la funzione di costruzione della classe base. Per fare questo dovremo iniziare a scrivere il codice helper. Questo è l'approccio più semplice che conosco:
function subclassOf(base) {
_subclassOf.prototype= base.prototype;
return new _subclassOf();
}
function _subclassOf() {};
Questo trasferisce i membri della classe base nel suo prototipo in una nuova funzione di costruzione che non fa nulla, quindi usa quel costruttore. Ora possiamo scrivere semplicemente:
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.prototype= subclassOf(Shape);
invece new Shape()
dell'errore. Ora abbiamo un insieme accettabile di primitive per le classi costruite.
Ci sono alcuni perfezionamenti ed estensioni che possiamo prendere in considerazione in questo modello. Ad esempio, ecco una versione a zucchero sintattico:
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);
Entrambe le versioni hanno lo svantaggio che la funzione di costruzione non può essere ereditata, come in molte lingue. Quindi, anche se la tua sottoclasse non aggiunge nulla al processo di costruzione, deve ricordare di chiamare il costruttore di base con qualunque argomento desiderasse la base. Questo può essere leggermente automatizzato usando apply
, ma devi ancora scrivere:
function Point() {
Shape.apply(this, arguments);
}
Point.subclass(Shape);
Quindi un'estensione comune è quella di suddividere l'inizializzazione nella propria funzione piuttosto che nel costruttore stesso. Questa funzione può quindi ereditare bene dalla 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!
Ora abbiamo appena la stessa funzione di costruttore per ogni classe. Forse possiamo spostarlo nella sua funzione helper in modo da non dover continuare a digitarlo, ad esempio invece di Function.prototype.subclass
capovolgerlo e lasciare che la funzione della classe base sputi sottoclassi:
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;
};
... che sta cominciando a sembrare un po 'più simile ad altre lingue, anche se con una sintassi leggermente più maldestra. Se lo desideri, puoi aggiungere alcune funzionalità extra. Forse vuoi makeSubclass
prendere e ricordare un nome di classe e fornire un valore predefinito toString
utilizzandolo. Forse vuoi fare in modo che il costruttore rilevi quando è stato accidentalmente chiamato senza l' new
operatore (che altrimenti comporterebbe spesso un fastidioso debug):
Function.prototype.makeSubclass= function() {
function Class() {
if (!(this instanceof Class))
throw('Constructor called without "new"');
...
Forse vuoi passare tutti i nuovi membri e makeSubclass
aggiungerli al prototipo, per Class.prototype...
evitare di dover scrivere abbastanza. Molti sistemi di classe lo fanno, ad esempio:
Circle= Shape.makeSubclass({
_init: function(x, y, z) {
Shape.prototype._init.call(this, x, y);
this.r= r;
},
...
});
Ci sono molte potenziali funzionalità che potresti considerare desiderabili in un sistema a oggetti e nessuno è veramente d'accordo su una formula particolare.
Il modo di chiusura , quindi. Ciò evita i problemi dell'ereditarietà basata sui prototipi di JavaScript, non usando l'ereditarietà. Anziché:
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();
Ora ogni singola istanza di Shape
avrà la propria copia del toString
metodo (e tutti gli altri metodi o altri membri della classe che aggiungiamo).
La cosa negativa di ogni istanza che ha la propria copia di ogni membro della classe è che è meno efficiente. Se hai a che fare con un gran numero di istanze di sottoclassi, l'ereditarietà prototipica potrebbe aiutarti meglio. Anche vedere un metodo della classe base è leggermente fastidioso come puoi vedere: dobbiamo ricordare quale era il metodo prima che il costruttore della sottoclasse lo sovrascrivesse, o si perde.
[Anche perché qui non c'è eredità, l' instanceof
operatore non funzionerà; dovresti fornire il tuo meccanismo per annusare la classe se ne hai bisogno. Mentre si potrebbe perdere tempo gli oggetti prototipo in un modo simile a quello con l'ereditarietà di prototipi, è un po 'complicato e non vale davvero la pena solo per ottenere instanceof
lavoro.]
La cosa buona di ogni istanza che ha il suo metodo è che il metodo può quindi essere associato all'istanza specifica che lo possiede. Ciò è utile a causa dello strano modo JavaScript di associare le this
chiamate ai metodi, che ha come risultato che se si stacca un metodo dal suo proprietario:
var ts= mycircle.toString;
alert(ts());
quindi this
all'interno del metodo non ci sarà l'istanza Circle come previsto (in realtà sarà l' window
oggetto globale , causando guai di debug diffusi). In realtà ciò accade in genere quando un metodo viene preso e assegnato a un setTimeout
, onclick
o EventListener
in generale.
Con il prototipo, devi includere una chiusura per ogni incarico del genere:
setTimeout(function() {
mycircle.move(1, 1);
}, 1000);
oppure, in futuro (o ora se si hackera Function.prototype) è possibile farlo anche con function.bind()
:
setTimeout(mycircle.move.bind(mycircle, 1, 1), 1000);
se le tue istanze vengono eseguite in modo chiusura, l'associazione viene eseguita gratuitamente dalla chiusura sulla variabile di istanza (di solito chiamata that
o self
, sebbene personalmente consiglierei contro quest'ultima poiché self
ha già un altro significato diverso in JavaScript). 1, 1
Tuttavia, non ottieni gli argomenti nello snippet gratuito, quindi avrai comunque bisogno di un'altra chiusura o di un bind()
se dovessi farlo.
Ci sono molte varianti anche sul metodo di chiusura. Puoi preferire omettere this
completamente, creando un nuovo that
e restituendolo invece di utilizzare l' new
operatore:
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
In che modo è "corretto"? Tutti e due. Qual è il "migliore"? Dipende dalla tua situazione. FWIW Tendo alla prototipazione per la vera eredità JavaScript quando sto facendo cose fortemente OO e chiusure per semplici effetti di pagina usa e getta.
Ma entrambi i modi sono abbastanza intuitivi per la maggior parte dei programmatori. Entrambi hanno molte potenziali variazioni disordinate. Incontrerai entrambi (così come molti schemi intermedi e generalmente rotti) se usi codice / librerie di altre persone. Non esiste una risposta generalmente accettata. Benvenuti nel meraviglioso mondo degli oggetti JavaScript.
[Questa è stata la parte 94 del motivo per cui JavaScript non è il mio linguaggio di programmazione preferito.]