Definizione di classi o oggetti ECMAScript
- Pagina precedente Oggetto di scope
- Pagina successiva Modifica dell'oggetto
L'uso di oggetti predefiniti è solo una parte delle capacità del linguaggio orientato agli oggetti, la sua vera forza risiede nella capacità di creare classi e oggetti personalizzati.
ECMAScript ha molti metodi per creare oggetti o classi.
Il metodo di fabbrica
Il metodo originale
Poiché le proprietà degli oggetti possono essere definite dinamicamente dopo la creazione dell'oggetto, molti sviluppatori hanno scritto codice simile al seguente quando JavaScript è stato introdotto per la prima volta:
var oCar = new Object; oCar.color = "blue"; oCar.doors = 4; oCar.mpg = 25; oCar.showColor = function() { alert(this.color); };
Nel codice sopra, viene creato l'oggetto car. Poi vengono impostate alcune proprietà: il suo colore è blu, ha quattro porte e può percorrere 25 miglia per gallone. L'ultima proprietà è un puntatore a funzione, il che significa che è un metodo. Dopo aver eseguito questo codice, è possibile utilizzare l'oggetto car.
Tuttavia, c'è un problema: potrebbe essere necessario creare più istanze di car.
Soluzione: metodo di fabbrica
Per risolvere questo problema, gli sviluppatori hanno creato funzioni di fabbrica che possono creare e restituire oggetti di tipo specifico.
Ad esempio, la funzione createCar() può essere utilizzata per encapsulare le operazioni di creazione dell'oggetto car elencate in precedenza:
function createCar() { var oTempCar = new Object; oTempCar.color = "blue"; oTempCar.doors = 4; oTempCar.mpg = 25; oTempCar.showColor = function() { alert(this.color); }; return oTempCar; } var oCar1 = createCar(); var oCar2 = createCar();
Qui, tutto il codice dell'esempio iniziale è contenuto nella funzione createCar(). Inoltre, c'è una riga di codice aggiuntiva che restituisce l'oggetto car (oTempCar) come valore della funzione. Chiudendo questa funzione, viene creato un nuovo oggetto e vengono assegnate tutte le proprietà necessarie, copiando l'oggetto car descritto in precedenza. Quindi, con questo metodo, possiamo facilmente creare due versioni di oggetti car (oCar1 e oCar2) con proprietà identiche.
Passare parametri alla funzione
Possiamo anche modificare la funzione createCar(), passandole valori di default per tutte le proprietà, non semplicemente assegnando valori di default alle proprietà:
function createCar(sColor,iDoors,iMpg) { var oTempCar = new Object; oTempCar.color = sColor; oTempCar.doors = iDoors; oTempCar.mpg = iMpg; oTempCar.showColor = function() { alert(this.color); }; return oTempCar; } var oCar1 = createCar("red",4,23); var oCar2 = createCar("blue",3,25); oCar1.showColor(); //Output "red" oCar2.showColor(); //Output "blue"
Aggiungendo parametri alla funzione createCar(), è possibile assegnare i valori delle proprietà color, doors e mpg dell'oggetto car da creare. Questo permette di avere due oggetti con le stesse proprietà ma valori diversi.
Definire i metodi dell'oggetto al di fuori della funzione di fabbrica
Anche se ECMAScript diventa sempre più formale, i metodi per creare oggetti vengono trascurati e la loro standardizzazione è ancora oggetto di opposizione. Parte è una ragione semantica (non sembra tanto regolare come usare l'operatore new con il costruttore), parte è una ragione funzionale. La ragione funzionale sta nel fatto che in questo modo è necessario creare metodi per oggetti. Negli esempi precedenti, ogni volta che si chiama la funzione createCar(), viene creato un nuovo funzione showColor(), il che significa che ogni oggetto ha la sua versione owna showColor(). In realtà, ogni oggetto condivide la stessa funzione.
Alcuni sviluppatori definiscono i metodi degli oggetti al di fuori delle funzioni di fabbrica e poi li puntano attraverso le proprietà per evitare questo problema:}
function showColor() { alert(this.color); } function createCar(sColor,iDoors,iMpg) { var oTempCar = new Object; oTempCar.color = sColor; oTempCar.doors = iDoors; oTempCar.mpg = iMpg; oTempCar.showColor = showColor; return oTempCar; } var oCar1 = createCar("red",4,23); var oCar2 = createCar("blue",3,25); oCar1.showColor(); //Output "red" oCar2.showColor(); //Output "blue"
Nel codice riscritto qui sopra, la funzione showColor() è definita prima della funzione createCar(). All'interno di createCar(), viene assegnato all'oggetto un puntatore alla funzione showColor() già esistente. Funzionalmente, questo risolve il problema della creazione ripetuta degli oggetti delle funzioni; ma semanticamente, la funzione non sembra molto come un metodo dell'oggetto.
Tutte queste questioni hanno portato aDefinito dallo sviluppatorel'apparizione del costruttore.
Metodo di costruzione
Creare un costruttore è facile come creare una funzione di fabbrica. Il primo passo è scegliere il nome della classe, ossia il nome del costruttore. Secondo la convenzione, la lettera iniziale è maiuscola per distinguerla dai nomi delle variabili che di solito sono minuscole. A parte questo, il costruttore sembra molto simile a una funzione di fabbrica. Considerate l'esempio seguente:
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.showColor = function() { alert(this.color); }; } var oCar1 = new Car("red",4,23); var oCar2 = new Car("blue",3,25);
Di seguito spieghiamo la differenza tra il codice e il metodo di fabbrica. Prima di tutto, nel costruttore non viene creato un oggetto, ma viene utilizzato il simbolo this. Quando si costruisce il costruttore utilizzando l'operatore new, viene creato un oggetto prima dell'esecuzione del primo codice, e solo utilizzando this è possibile accedere a quell'oggetto. Poi è possibile assegnare direttamente le proprietà di this, di default è il valore di ritorno del costruttore (non è necessario utilizzare esplicitamente l'operatore return).
Ora, la creazione di oggetti utilizzando l'operatore new e il nome della classe Car è più simile alla creazione di oggetti generali ECMAScript.
Potresti chiederti se questo metodo ha lo stesso problema di gestione delle funzioni del primo metodo. Sì.
Come le funzioni di fabbrica, i costruttori generano funzioni ripetutamente, creando una versione di funzione indipendente per ogni oggetto. Tuttavia, come le funzioni di fabbrica, è possibile sovrascrivere il costruttore con una funzione esterna, il che non ha alcun senso semanticamente. Questo è proprio l'aspetto più vantaggioso del metodo prototipale.
Il metodo prototipale
Questo metodo utilizza l'attributo prototype dell'oggetto, che può essere considerato il原型 che l'oggetto dipende per creare un nuovo oggetto.
Qui, si utilizza un costruttore vuoto per impostare il nome della classe. Poi, tutte le proprietà e i metodi vengono assegnati direttamente all'attributo prototype. Abbiamo riscritto l'esempio precedente, il codice è il seguente:
function Car() { } Car.prototype.color = "blue"; Car.prototype.doors = 4; Car.prototype.mpg = 25; Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car(); var oCar2 = new Car();
In questo pezzo di codice, prima si definisce il costruttore (Car), che non contiene alcun codice. Le successive righe di codice aggiungono proprietà all'attributo prototype di Car per definire le proprietà dell'oggetto Car. Quando si chiama new Car(), tutte le proprietà del prototipo vengono immediatamente assegnate all'oggetto da creare, il che significa che tutte le istanze di Car memorizzano puntatori alla funzione showColor(). Dal punto di vista semantico, tutte le proprietà sembrano appartenere a un singolo oggetto, il che risolve i problemi delle due precedenti modalità.
Inoltre, utilizzando questo metodo, è possibile utilizzare l'operatore instanceof per verificare il tipo dell'oggetto a cui punta una variabile. Pertanto, il seguente codice outputterà TRUE:
alert(oCar1 instanceof Car); // Output: "true"
I problemi del metodo prototipale
Il metodo prototipale sembra essere una buona soluzione. Purtroppo, non è proprio come ci si aspetta.
Prima di tutto, questa funzione costruttore non accetta parametri. Utilizzando il metodo prototipale, non è possibile inizializzare i valori delle proprietà passando parametri al costruttore, perché le proprietà color di Car1 e Car2 sono entrambe "blue", le proprietà doors sono entrambe 4, e le proprietà mpg sono entrambe 25. Questo significa che i valori predefiniti delle proprietà devono essere modificati dopo la creazione dell'oggetto, il che è piuttosto fastidioso, ma c'è di più. Il vero problema si verifica quando le proprietà puntano a oggetti invece che a funzioni. La condivisione delle funzioni non crea problemi, ma gli oggetti vengono raramente condivisi tra più istanze. Pensa all'esempio seguente:
function Car() { } Car.prototype.color = "blue"; Car.prototype.doors = 4; Car.prototype.mpg = 25; Car.prototype.drivers = new Array("Mike","John"); Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car(); var oCar2 = new Car(); oCar1.drivers.push("Bill"); alert(oCar1.drivers); //Output: "Mike,John,Bill" alert(oCar2.drivers); //Output: "Mike,John,Bill"
Nel codice sopra, la proprietà drivers è un puntatore all'oggetto Array, che contiene due nomi "Mike" e "John". Poiché drivers è un valore di riferimento, entrambi gli istanzi di Car puntano allo stesso array. Questo significa che aggiungere il valore "Bill" a oCar1.drivers, può essere visto anche in oCar2.drivers. L'output di uno qualsiasi di questi due puntatori mostrerà la stringa "Mike,John,Bill".
Poiché ci sono così tanti problemi nella creazione di oggetti, ti starai chiedendo se esista un metodo ragionevole per creare oggetti. La risposta è sì, è necessario utilizzare in combinazione costruttori e metodo prototipale.
Metodo misto costruttore/prototipale
L'uso combinato di costruttori e metodo prototipale permette di creare oggetti come in altri linguaggi di programmazione. Questo concetto è molto semplice: utilizzare il costruttore per definire tutte le proprietà non funzionali dell'oggetto, e il metodo prototipale per definire le proprietà funzionali (metodi) dell'oggetto. Di conseguenza, tutte le funzioni vengono create una sola volta, e ogni oggetto ha la sua istanza di proprietà oggetto.
Abbiamo rivisitato l'esempio precedente, il codice è il seguente:
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.drivers = new Array("Mike","John"); } Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car("red",4,23); var oCar2 = new Car("blue",3,25); oCar1.drivers.push("Bill"); alert(oCar1.drivers); //Output: "Mike,John,Bill" alert(oCar2.drivers); //Output: "Mike,John"
Ora è ancora più simile alla creazione di oggetti generici. Tutte le proprietà non funzionali vengono create nel costruttore, il che significa che possiamo di nuovo assegnare valori predefiniti ai parametri del costruttore. Poiché viene creato solo un'istanza della funzione showColor(), non ci sono sprechi di memoria. Inoltre, aggiungere il valore "Bill" all'array drivers di oCar1 non influisce sull'array di oCar2, quindi quando si escono i valori degli array, oCar1.drivers mostra "Mike,John,Bill", mentre oCar2.drivers mostra "Mike,John". Poiché viene utilizzato il metodo prototipale, è ancora possibile utilizzare l'operatore instanceof per determinare il tipo dell'oggetto.
Questo è il metodo principale adottato da ECMAScript, che ha le caratteristiche di altri metodi, ma senza i loro effetti collaterali. Tuttavia, alcuni sviluppatori pensano che questo metodo non sia perfetto.
Metodo di prototipo dinamico
Per gli sviluppatori abituati a utilizzare altre lingue, l'uso del metodo costruttore misto/prototipo non sembra molto armonioso. Dopo tutto, quando si definisce una classe, la maggior parte dei linguaggi orientati agli oggetti chiude visivamente proprietà e metodi. Considera il seguente esempio di classe Java:
class Car { public String color = "blue"; public int doors = 4; public int mpg = 25; public Car(String color, int doors, int mpg) { this.color = color; this.doors = doors; this.mpg = mpg; } public void showColor() { System.out.println(color); } }
Java impacchetta bene tutte le proprietà e i metodi della classe Car, quindi vedendo questo codice si può sapere cosa deve fare, definendo le informazioni dell'oggetto. Chi critica il metodo costruttore misto/prototipo pensa che sia logico cercare proprietà all'interno del costruttore e metodi all'esterno. Pertanto, hanno progettato il metodo di prototipo dinamico per fornire uno stile di codifica più amichevole.
L'idea di base del metodo di prototipo dinamico è la stessa di quella del costruttore misto/prototipo, ovvero definire attributi non funzionali all'interno del costruttore e attributi funzionali utilizzando attributi del prototipo. L'unica differenza è la posizione assegnata ai metodi dell'oggetto. Di seguito è riportata la classe Car riscritta utilizzando il metodo di prototipo dinamico:
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.drivers = new Array("Mike","John"); if (typeof Car._initialized == "undefined") { Car.prototype.showColor = function() { alert(this.color); }; Car._initialized = true; } }
Questa funzione costruttore non è cambiata fino a quando typeof Car._initialized non è uguale a "undefined". Questa è la parte più importante dei metodi di原型dinamici. Se questo valore non è definito, il costruttore continua a definire i metodi dell'oggetto in modo prototipale e poi imposta Car._initialized a true. Se questo valore è definito (il suo valore è true, typeof restituisce Boolean), non viene creata questa metodo. In breve, questo metodo utilizza un flag (_initialized) per determinare se sono stati assegnati qualsiasi metodo al prototipo. Questo metodo viene creato e assegnato una sola volta e i tradizionali sviluppatori OOP saranno felici di notare che questo codice sembra più una definizione di classe in altri linguaggi.
Metodo fabbrica misto
Questo metodo è solitamente una soluzione di compromesso quando non è possibile applicare il metodo precedente. Lo scopo è creare un falso costruttore che restituisce solo una nuova istanza di un altro oggetto.
Questo codice sembra molto simile a una funzione fabbrica:
function Car() { var oTempCar = new Object; oTempCar.color = "blue"; oTempCar.doors = 4; oTempCar.mpg = 25; oTempCar.showColor = function() { alert(this.color); }; return oTempCar; }
Diversamente dal metodo classico, questo metodo utilizza l'operatore new, rendendolo simile a un vero costruttore:
var car = new Car();
Poiché l'operatore new è chiamato all'interno del costruttore Car(), l'operatore new secondario (posto al di fuori del costruttore) viene ignorato e l'oggetto creato all'interno del costruttore viene passato alla variabile car.
Questo metodo ha gli stessi problemi di gestione interna dei metodi degli oggetti rispetto al metodo classico. Si consiglia vivamente: evita di utilizzare questo metodo a meno che non sia assolutamente necessario.
Quale metodo utilizzare
Come menzionato in precedenza, il metodo più diffuso è la combinazione di costruttore/pro原型方式. Inoltre, i metodi originali dinamici sono molto popolari e equivalgono funzionalmente al metodo costruttore/pro原型. Puoi utilizzare uno di questi due metodi. Tuttavia, evita di utilizzare il classico costruttore o il metodo pro singolarmente, poiché ciò potrebbe introdurre problemi nel codice.
Esempio
Un aspetto interessante degli oggetti è il modo in cui risolvono problemi. Uno dei problemi più comuni in ECMAScript è la performance della concatenazione delle stringhe. Allo stesso modo di altri linguaggi, le stringhe in ECMAScript sono immutabili, il che significa che il loro valore non può essere modificato. Considera il seguente codice:
var str = "hello "; str += "world";
In realtà, i passaggi eseguiti dietro le quinte sono i seguenti:
- Creare una stringa per memorizzare "hello ";
- Creare una stringa per memorizzare "world".
- Creare una stringa per memorizzare il risultato della concatenazione.
- Copiare il contenuto attuale di str nel risultato.
- Copiare "world" nel risultato.
- Aggiornare str in modo che punti al risultato.
Ogni volta che si completa la concatenazione delle stringhe si esegue il passaggio 2 a 6, rendendo questa operazione molto consumatrice di risorse. Se si ripete questo processo centinaia o anche migliaia di volte, si possono verificare problemi di prestazioni. La soluzione è memorizzare le stringhe in un oggetto Array e poi creare la stringa finale con il metodo join() (parametro è una stringa vuota). Immaginate di sostituire il codice precedente con il seguente:
var arr = new Array(); arr[0] = "hello "; arr[1] = "world"; var str = arr.join("");
In questo modo, non c'è problema con l'inserimento di quante più stringhe ci si voglia, perché la concatenazione avviene solo quando si chiama il metodo join(). Al momento, i passaggi eseguiti sono i seguenti:
- Creare una stringa per memorizzare il risultato
- Copiare ogni stringa nella posizione appropriata del risultato
Anche se questa soluzione è buona, c'è un metodo migliore. Il problema è che questo codice non riflette esattamente il suo scopo. Per renderlo più facile da capire, si può impacchettare questa funzionalità nella classe StringBuffer:
function StringBuffer () { this._strings_ = new Array(); } StringBuffer.prototype.append = function(str) { this._strings_.push(str); }; StringBuffer.prototype.toString = function() { return this._strings_.join(""); };
L'attenzione principale di questo codice è l'attributo strings, che è un attributo privato. Ha solo due metodi, ovvero i metodi append() e toString(). Il metodo append() ha un parametro, che aggiunge il parametro alla stringa dell'array, mentre il metodo toString() chiama il metodo join() dell'array per restituire la stringa veramente concatenata. Per concatenare una serie di stringhe con l'oggetto StringBuffer, si può usare il seguente codice:
var buffer = new StringBuffer(); buffer.append("hello "); buffer.append("world"); var result = buffer.toString();
Esempi di codice sotto possono essere utilizzati per testare le prestazioni dell'oggetto StringBuffer rispetto al metodo di concatenazione tradizionale delle stringhe:
var d1 = new Date(); var str = ""; for (var i=0; i < 10000; i++) { str += "text"; } var d2 = new Date(); document.write("Concatenazione con plus: ") + (d2.getTime() - d1.getTime()) + " milliseconds"); var buffer = new StringBuffer(); d1 = new Date(); for (var i=0; i < 10000; i++) { buffer.append("text"); } var result = buffer.toString(); d2 = new Date(); document.write("<br />Concatenazione con StringBuffer: ") + (d2.getTime() - d1.getTime()) + " milliseconds");
Questo codice esegue due test di concatenazione di stringhe, il primo utilizzando il segno più, il secondo utilizzando la classe StringBuffer. Ogni operazione concatena 10000 di stringhe. Le date d1 e d2 vengono utilizzate per determinare il tempo necessario per completare l'operazione. Si prega di notare che se non viene fornito alcun parametro durante la creazione dell'oggetto Date, viene assegnato all'oggetto la data e l'ora corrente. Per calcolare quanto tempo è necessario per la concatenazione, è sufficiente sottrarre i valori in millisecondi (ottenuti tramite il metodo getTime()) delle date. Questo è un metodo comune per misurare le prestazioni di JavaScript. I risultati del test possono aiutarti a confrontare l'efficienza di utilizzare la classe StringBuffer rispetto al segno più.
- Pagina precedente Oggetto di scope
- Pagina successiva Modifica dell'oggetto