ECMAScript definere klasse eller objekt

Brug af foruddefinerede objekter er kun en del af et objektorienteret sprog, men det virkelig kraftige ved det er evnen til at skabe egne specialiserede klasser og objekter.

ECMAScript har mange metoder til at oprette objekter eller klasser.

Fabriksmetode

Original måde

Fordi objektets egenskaber kan defineres dynamisk efter objektets oprettelse, skriver mange udviklere kode lignende nedenfor ved introduktionen af JavaScript:

var oCar = new Object;
oCar.color = "blue";
oCar.doors = 4;
oCar.mpg = 25;
oCar.showColor = function() {
  alert(this.color);
};

TIY

I det ovenstående kode skabes objektet car. Derefter indstilles nogle egenskaber: dens farve er blå, den har fire døre, og den kan køre 25 miles pr. gallon. Den sidste egenskab er en pege på en funktion, hvilket betyder, at egenskaben er en metode. Efter at have kørt dette kode kan man bruge objektet car.

Men der er et problem, nemlig at det kan være nødvendigt at oprette flere car instanser.

Løsning: Fabrikmetoden

For at løse dette problem har udviklere skabt fabrikkefunktioner, der kan oprette og returnere specifikke objekttyper.

For eksempel kan funktionen createCar() bruges til at indpakke de operationer til at oprette car objekter, der er nævnt tidligere:

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();

TIY

Her er alle koderne i den første eksempel inkluderet i funktionen createCar(). Derudover er der en ekstra linje kode, der returnerer car objektet (oTempCar) som funktionens værdi. Ved at kalde denne funktion, oprettes et nyt objekt, og给它 alle nødvendige egenskaber, kopierer en car objekt, vi har beskrevet tidligere. På denne måde kan vi nemt oprette to versioner af car objekter (oCar1 og oCar2), der har helt ens egenskaber.

Passer parametre til funktionen

Vi kan også ændre funktionen createCar(), og give den standardværdier for de forskellige egenskaber, i stedet for kun at tildel standardværdier til egenskaberne:

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();		//Udskriver "red"
oCar2.showColor();		//Udskriver "blue"

TIY

Tilføj parametre til funktionen createCar(), så kan man tildele værdier til farve, døre og mpg egenskaberne for det at oprette car objekt. Dette gør, at to objekter har de samme egenskaber, men med forskellige værdier.

Definer objektets metoder uden for fabrikken

Selvom ECMAScript bliver mere formaliseret, ignoreres metoderne til at oprette objekter, og deres standardisering bliver stadig modarbejdet. En del skyldes semantiske årsager (det ser ikke ud som om det er så formelt at bruge den med konstruktøren new), og en del skyldes funktionelle årsager. Funktional årsag er, at man med denne metode skal oprette objekter. I de tidligere eksempler, hver gang man kalder funktionen createCar(), oprettes en ny funktion showColor(), hvilket betyder, at hver objekt har sin egen version af showColor(). Og faktisk, har alle objekter samme funktion.

Nogle udviklere definerer objektets metoder uden for fabrikfunktionen og peger derefter på denne metode gennem en egenskab for at undgå dette problem:

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();		//Udskriver "red"
oCar2.showColor();		//Udskriver "blue"

TIY

I det her ændrede kodeafsnit er funktionen showColor() defineret før funktionen createCar(). Inden for createCar() tildeler man objektet en pege på den allerede eksisterende showColor()-funktion. Funktionelt set løser dette problem med gentagne oprettelser af funktionsobjekter; men semantisk set ligner det ikke rigtig som en objektmetode.

alle disse problemer har ført tiludviklerdefineretoptrædelsen af konstruktøren.

Konstruktørmetoden

At oprette en konstruktør er ligeså nemt som at oprette en fabrikfunktion. Første skridt er at vælge klassenavn, altså navnet på konstruktøren. Ifølge konvention skal dette navn starte med en stor bogstav for at adskille det fra variabler, der normalt starter med en lille bogstav. Udover dette ligner konstruktøren meget en fabrikfunktion. Overvej følgende eksempel:

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);

TIY

Her forklarer vi forskellen på ovenstående kode og fabriksmetoden. Først oprettes der ikke et objekt i konstruktøren, men i stedet bruges this-kuglen. Når konstruktøren oprettes med new-betegnelsen, oprettes et objekt først før første linje kode udføres, og kun med this kan man få adgang til dette objekt. Derefter kan man direkte tildele this-egenskaber, som standard er konstruktørens tilbageværdi (der er ingen grund til at bruge return-betegnelsen).

Nå, ved å bruke new-operatoren og klassenavnet Car for å opprette objekter, blir det mer som opprettelse av vanlige ECMAScript-objekter.

Du kan kanskje spørre om det finnes samme problem med funksjonsstyring som med den forrige metoden? Ja.

Lignende som en fabrikkfunksjon, vil konstruktøren gjenopprette funksjoner, og opprette en uavhengig funksjonsversjon for hver objekt. Men, som med fabrikkfunksjoner, kan man også skrive om konstruktøren med en ekstern funksjon, noe som ikke har noen semantisk mening. Dette er akkurat det vi skal snakke om med fordelene ved prototype-metoden.

Prototype-metoden

Denne metoden utnytter objektets prototype-egenskap, som kan ses på som den prototypen som avhenger av når en ny objekt opprettes.

Her brukes en tom konstruktør for å sette klassenavnet. Deretter blir alle egenskaper og metoder direkte tildelt prototype-egenskapen. Vi har omdefinert det tidligere eksempelet, og koden ser slik ut:

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();

TIY

I denne koden defineres først konstruktøren (Car), uten noen kode. De neste linjene legger til egenskaper til Car sin prototype-egenskap for å definere egenskapene til Car-objektet. Når new Car() kalles, blir alle prototypens egenskaper umiddelbart tildelt det objektet som skal opprettes, noe som betyr at alle Car-instanser inneholder peker til showColor()-funksjonen. Semantisk sett ser alle egenskaper ut til å tilhøre et objekt, og dette løser de to første problemene vi hadde.

Ved denne metoden kan man også bruke instanceof-operatoren for å kontrollere typen på objektet som en variabel peker mot. Derfor vil følgende kode utskrive TRUE:

alert(oCar1 instanceof Car);	//Udskriver "true"

Problemer med prototype-metoden

Prototype-metoden ser ut til å være en god løsning. Dessverre, det er ikke helt tilfredsstillende.

Først og fremmest har denne konstruktøren ingen parametre. Ved bruk av prototype-metoden kan man ikke initialisere egenskapene ved å overføre parametere til konstruktøren, fordi fargeegenskapen til Car1 og Car2 er "blue", dørgenskapet er 4, og mpg-egenskapen er 25. Dette betyr at egenskapene må endres etter at objektet er opprettet, noe som er ganske irriterende, men det er ikke slutt ennå. Det virkelige problemet oppstår når egenskapene peker mot objekter i stedet for funksjoner. Funksjoner deler ikke, men objekter gjør det sjelden. Tenk på følgende eksempel:

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);	//Udskriver "Mike,John,Bill"
alert(oCar2.drivers);	//Udskriver "Mike,John,Bill"

TIY

I ovenstående kode er egenskaben drivers en pejling til en Array-objekt, der indeholder to navne "Mike" og "John". Fordi drivers er en referencewert, henviser begge instanser af Car til den samme array. Dette betyder, at hvis man tilføjer værdien "Bill" til oCar1.drivers, kan man også se den i oCar2.drivers. Udskriver man hvilken som helst af disse pejlinger, vises strengen "Mike,John,Bill" som resultat.

På grund af alle de problemer, der opstår ved oprettelse af objekter, må du sikkert tænke over, om der findes en rimelig metode til at oprette objekter? Svaret er ja, og det kræver brug af både konstruktøren og prototype-metoden.

Blandet konstruktør/prototype-metode

Blandet brug af konstruktøren og prototype-metoden, kan man oprette objekter på samme måde som i andre programmeringssprog. Konceptet er meget simpelt: brug konstruktøren til at definere alle ikke-funktionelle egenskaber på objektet, og brug prototype-metoden til at definere funktionelle egenskaber (metoder) på objektet. Resultatet er, at alle funktioner oprettes kun én gang, og hver objektinstans har sin egen instans af objektegenskaber.

Vi har omskrevet det tidligere eksempel, og koden ser sådan ud:

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);	//Udskriver "Mike,John,Bill"
alert(oCar2.drivers);	//Udskriver "Mike,John"

TIY

Nu er det mere som at oprette almindelige objekter. Alle ikke-funktionelle egenskaber oprettes i konstruktøren, hvilket betyder, at vi igen kan tildelle standardværdier til egenskaberne via konstruktørens parametre. Da kun en instans af showColor() funktionen oprettes, er der ingen hukommelsesudnyttelse. Derudover påvirker tilføjelsen af "Bill" til oCar1's drivers-array ikke oCar2's array, så når der udskrives værdierne af disse arrayer, viser oCar1.drivers "Mike,John,Bill", mens oCar2.drivers viser "Mike,John". Fordi vi bruger prototype-metoden, kan vi stadig bruge instanceof-begrebet til at afgøre objektets type.

This is the main method adopted by ECMAScript, which has the characteristics of other methods without their side effects. However, some developers still think that this method is not perfect.

Dynamic prototype methods

For developers accustomed to using other languages, using the mixed constructor/proto method may not feel very harmonious. After all, when defining a class, most object-oriented languages visually encapsulate properties and methods. Consider the following Java class:

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 packages all the properties and methods of the Car class very well, so you can see from this code what it is supposed to do, it defines the information of an object. Critics of the mixed constructor/proto method argue that it is illogical to find properties inside the constructor and methods outside of it. Therefore, they designed dynamic prototype methods to provide a more friendly coding style.

Dynamic prototype methods have the same basic idea as mixed constructor/proto methods, that is, non-function properties are defined within the constructor, while function properties are defined using prototype properties. The only difference is the position where the object methods are assigned. Below is the Car class rewritten using dynamic prototype methods:

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;
  }
}

TIY

Konstruktørfunktionen ændres ikke, indtil typeof Car._initialized er lig med "undefined" først. Dette er den vigtigste del af den dynamiske prototype-metode. Hvis denne værdi ikke er defineret, fortsætter konstruktørfunktionen med at definere objektets metoder ved prototyperne og sætter Car._initialized til true. Hvis denne værdi er defineret (og værdien er true, når typeof's værdi er Boolean), oprettes metoden ikke. Kort sagt bruger denne metode et flag (_initialized) til at afgøre, om der allerede er givet nogen metoder til prototypen. Metoden oprettes og tildelingen sker kun én gang, hvilket vil glæde traditionelle OOP-udviklere, da koden ser mere ud som en klassedefinition i andre sprog.

Blandet fabrikmetode

Denne metode bruges ofte som en alternativ løsning, når den tidligere metode ikke kan anvendes. Dets formål er at oprette en falsk konstruktørfunktion, der kun returnerer en ny instans af et andet objekt.

Dette kodeeksempel ser meget lig et fabrikfunktion:

function Car() {
  var oTempCar = new Object;
  oTempCar.color = "blue";
  oTempCar.doors = 4;
  oTempCar.mpg = 25;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}

TIY

Forskellig fra den klassiske metode bruger denne metode new-operatoren, hvilket gør det udseende som en rigtig konstruktørfunktion:

var car = new Car();

Da new-operatoren bruges inden for Car()-konstruktørfunktionen, ignoreres den anden new-operatoren (uden for konstruktørfunktionen), og det objekt, der oprettes inden for konstruktørfunktionen, overføres til variablen car.

Denne metode har de samme problemer med intern styring af objektmetoder som den klassiske metode. Det anbefales kraftigt: Unngå denne metode medmindre det er absolut nødvendigt.

Hvilken metode skal man vælge

Som nævnt tidligere er det mest almindelige at bruge en kombination af konstruktørfunktioner og prototyper. Derudover er dynamiske primitive metoder også meget populære og fungerer funktionelt set på samme måde som konstruktørfunktioner og prototyper. Man kan bruge enten af disse to måder. Dog bør man undgå at bruge klassiske konstruktørfunktioner eller prototyper alene, da dette kan føre til problemer i koden.

Eksempel

En interessant ting ved objekter er måden de løser problemer på. En af de mest almindelige problemer i ECMAScript er ydeevnen ved strengkobling. Som med andre sprog er ECMAScript's strenge uændelige, hvilket betyder, at deres værdi ikke kan ændres. Overvej følgende kode:

var str = "hello ";
str += "world";

Faktisk udfører dette kode segment følgende skjulte trin:

  1. Opret en streng til at gemme "hello ".
  2. Opret en streng til at gemme "world".
  3. Opret en streng til at gemme den forbundne resultat.
  4. Kopier str's nuværende indhold til resultatet.
  5. Kopier "world" til resultatet.
  6. Opdater str, så den peger på resultatet.

Hver gang en strengforbindelse udføres, udføres trin 2 til 6, hvilket gør denne operation meget ressourcekrævende. Hvis denne proces gentages hundredvis eller tusindvis af gange, kan det medføre ydeevneproblemer. En løsning er at gemme strengene i et Array objekt og derefter oprette den endelige streng ved hjælp af join() metoden (med et tomt tegn som parameter). Forestil dig at erstatte den tidligere kode med følgende kode:

var arr = new Array();
arr[0] = "hello ";
arr[1] = "world";
var str = arr.join("");

Så, uanset hvor mange strengobjekter der tilføjes til listen, er der ingen problemer, fordi forbindelsesoperationen kun udføres ved kald af join() metoden. De udførte trin er som følger:

  1. Opret en streng til at gemme resultatet
  2. Kopier hver streng til den passende position i resultatet

Selvom denne løsning er god, findes der bedre metoder. Problemet er, at dette kode segment ikke præcist reflekterer dens intention. For at gøre det lettere at forstå kan man pakke denne funktion ind i en StringBuffer klasse:

function StringBuffer () {
  this._strings_ = new Array();
}
StringBuffer.prototype.append = function(str) {
  this._strings_.push(str);
};
StringBuffer.prototype.toString = function() {
  return this._strings_.join("");
};

Dette kode段首先 skal man bemærke er strings egenskaben, som er en privat egenskab. Den har kun to metoder, nemlig append() og toString() metoderne. append() metoden har en parameter, som tilføjer parameteren til strenglisten, toString() metoden kalder listenens join metode og returnerer den faktisk forbundne streng. For at forbinde en gruppe af strengobjekter med en StringBuffer objekt kan man bruge følgende kode:

var buffer = new StringBuffer();
buffer.append("hello ");
buffer.append("world");
var result = buffer.toString();

TIY

Du kan teste ydeevnen af StringBuffer-objektet og den traditionelle strengkoblingsmetode med følgende kode:

var d1 = new Date();
var str = "";
for (var i=0; i < 10000; i++) {
    str += "text";
}
var d2 = new Date();
document.write("Concatenation with 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 />Concatenation with StringBuffer: ")
 + (d2.getTime() - d1.getTime()) + " milliseconds");

TIY

Dette kodeeksempel udfører to test af strengkobling, den ene bruger plus-tegnet, den anden bruger StringBuffer-klassen. Hver operation kobler 10000 strengene sammen. Datoerne d1 og d2 bruges til at vurdere, hvor lang tid det tager at fuldføre operationen. Bemærk, at hvis der ikke gives nogen parameter til at oprette en Date-objekt, tildeler objektet den nuværende dato og klokkeslæt. For at beregne, hvor lang tid det tager at fuldføre koblingsoperationen, kan man trække de millisekunder, der repræsenterer datoerne (med getTime() metodes returværdi), fra hinanden. Dette er en almindelig metode til at måle JavaScript's ydeevne. Testresultaterne kan hjælpe dig med at sammenligne effektiviteten af at bruge StringBuffer-klassen med at bruge plus-tegnet.