Definiera klass eller objekt i ECMAScript

Att använda fördefinierade objekt är bara en del av ett objektorienterat språks förmåga, dess verkliga styrka ligger i förmågan att skapa egna specialiserade klasser och objekt.

ECMAScript har många metoder för att skapa objekt eller klasser.

Fabrikmetod

Original metod

Eftersom objektets egenskaper kan definieras dynamiskt efter att objektet har skapats, skriver många utvecklare kod liknande följande när JavaScript först introducerades:

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

TIY

I ovanstående kod skapas objektet car. Sedan sätts några egenskaper: dess färg är blå, det har fyra dörrar och kan köra 25 miles per gallon. Den sista egenskapen är en pekpunkt till en funktion, vilket innebär att egenskapen är en metod. Efter att ha kört detta kodsegment kan objektet car användas.

Men här finns ett problem, nämligen att det kan behövas skapa flera instanser av car.

Lösning: Fabrikmetod

För att lösa detta problem har utvecklare skapat fabrikfunktioner som kan skapa och returnera specifika typer av objekt.

Till exempel kan funktionen createCar() användas för att innesluta de operationer för att skapa car-objekt som listas tidigare:

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

Här innehåller all kod från det första exemplet i funktionen createCar(). Dessutom finns det en extra rad kod som returnerar car-objektet (oTempCar) som funktionens värde. Genom att anropa denna funktion skapas ett nytt objekt som tilldelas alla nödvändiga egenskaper och kopierar ett car-objekt som vi beskrev tidigare. På detta sätt kan vi enkelt skapa två versioner av car-objektet (oCar1 och oCar2) med helt identiska egenskaper.

Skicka med parametrar till funktionen

Vi kan också modifiera funktionen createCar() och skicka med standardvärden för alla egenskaper istället för att bara tilldela standardvärden till egenskaperna:

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"

TIY

Lägg till parametrar till funktionen createCar() för att tilldela färg, dörrar och mpg-attributen för det att skapa car-objektet. Detta gör att två objekt har samma egenskaper men olika egenskapsvärden.

Definiera objektets metoder utanför fabrikfunktionen

Trots att ECMAScript blir alltmer formelliserad, har metoder för att skapa objekt blivit försummade, och dess standardisering fortfarande möter motstånd. En del är av semantisk anledning (det ser inte ut som att använda en konstruktionsfunktion med new-operatorn är lika formellt), och en del är av funktionsanledning. Funktionsanledningen ligger i att det måste skapas objekt på detta sätt. I de tidigare exemplen måste en ny funktion showColor() skapas varje gång funktionen createCar() anropas, vilket innebär att varje objekt har sin egen version av showColor(). Men faktiskt delar alla objekt samma funktion.

Vissa utvecklare definierar objektets metoder utanför fabrikfunktionen och pekar sedan på metoden via egenskapen för att undvika detta 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();		//Output "red"
oCar2.showColor();		//Output "blue"

TIY

I detta omkallade kodsegment definieras funktionen showColor() innan funktionen createCar(). Inuti createCar() tilldelas objektet en pekpunkter till den redan existerande funktionen showColor(). Funktionellt sett löser detta problemet med att skapa flera funktioner, men semantiskt verkar det inte som en metod för objektet.

alla dessa problem har lett tillutvecklare definieraruppkomst av konstruktorn.

Konstruktionsmetod

Att skapa en konstruktorn är lika enkelt som att skapa en fabrikfunktion. Steg ett är att välja klassnamn, det vill säga namnet på konstruktorn. Enligt konvention är det första bokstaven stor för att skilja sig från variabler som vanligtvis har små bokstäver. Utöver detta liknar konstruktorn mycket en fabrikfunktion. Överväg följande exempel:

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

Här förklarar vi skillnaden mellan ovanstående kod och fabrikmetoden. Först skapar vi inte objektet inom konstruktorn, utan använder this-nyckelordet. När vi konstruerar en funktion med new-operatorn skapas ett objekt innan det första raden körs, och bara med this kan vi komma åt detta objekt. Därefter kan vi direkt tilldela this-attribut, som standard är det tillbakavärdet från konstruktorn (det är inte nödvändigt att tydligt använda return-operatorn).

Nu skapar du objekt med new-operatorn och klassnamnet Car på ett sätt som är mer likt skapandet av vanliga ECMAScript-objekt.

Du kanske frågar om detta sätt har samma problem som det föregående sättet när det gäller att hantera funktioner? Ja.

Som en fabrikffunktion skapar konstruktionsfunktionen upprepade gånger funktioner, skapar en separat funktion för varje objekt. Även om det liknar fabrikffunktioner, kan du också skriva om konstruktionsfunktionen med en extern funktion, vilket semantiskt sett inte har någon mening. Detta är precis fördelen med prototypmetoden.

Prototypmetoden

Denna metod använder objektets prototype-attribut, som kan ses som den prototyp som används för att skapa nya objekt.

Här definieras först en tom konstruktionsfunktion för att sätta klassnamnet. Därefter tilldelas alla egenskaper och metoder direkt till prototype-attributet. Vi har omformulerat det tidigare exemplet, och koden ser ut så här:

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 detta stycke definieras först konstruktionsfunktionen (Car), vilket inte innehåller någon kod. De följande raderna lägger till egenskaper till Car-prototypen för att definiera egenskaperna för Car-objektet. När new Car() anropas, tilldelas alla prototypens egenskaper omedelbart till det objekt som skapas, vilket innebär att alla Car-instanser lagrar pekar till showColor-funktionen. Semantiskt sett verkar alla egenskaper tillhöra ett objekt, vilket löser problemen med de tidigare två metoderna.

Dessutom kan du använda denna metod för att kontrollera objekttypen på den givna variabeln med hjälp av instanceof-operatorn. Därför kommer följande kod att output TRUE:

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

Problem med prototypmetoden

Prototypmetoden verkar vara ett bra lösning. Tyvärr, det är inte riktigt så.

Först och främst har denna konstruktionsfunktion inga parametrar. Användandet av prototypmetoden gör det inte möjligt att initialisera egenskapsvärden genom att skicka parametrar till konstruktionsfunktionen, eftersom color-egenskapen för Car1 och Car2 är lika med "blue", doors-egenskapen är lika med 4, och mpg-egenskapen är lika med 25. Detta innebär att det måste vara möjligt att ändra standardvärdena för egenskaperna efter att objektet har skapats, vilket är ganska irriterande, men ännu inte slutet. Det verkliga problemet uppstår när egenskaperna pekar på objekt, inte funktioner. Funksionsdelning orsakar inga problem, men objekt delas sällan mellan flera instanser. Låt oss tänka på följande exempel:

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

TIY

I den ovanstående koden är egenskapen drivers en pekare till ett Array-objekt som innehåller två namn "Mike" och "John". Eftersom drivers är en referensvärde pekar båda instanserna av Car på samma array. Detta innebär att när du lägger till värdet "Bill" till oCar1.drivers, kan du också se det i oCar2.drivers. Utskriften av vilken som helst av dessa pekare visar strängen "Mike,John,Bill".

Eftersom det finns så många problem när vi skapar objekt, kanske du funderar på om det finns ett rimligt sätt att skapa objekt. Svaret är ja, och det kräver att vi kombinerar konstruktörer och prototypmetoder.

Blandad konstruktör/prototypmetod

Genom att kombinera konstruktörer och prototypmetoder kan vi skapa objekt på samma sätt som i andra programmeringsspråk. Detta koncept är mycket enkelt: använd konstruktören för att definiera alla icke-funktionella egenskaper hos objektet, och använd prototypmetoden för att definiera objektets funktionella egenskaper (metoder). Resultatet är att alla funktioner skapas endast en gång, och varje objekt har sina egna instanser av objektegenskaper.

Vi har rewritten det tidigare exemplet, koden ser ut så här:

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

TIY

Nu är det mer som att skapa en vanlig objekt. Alla icke-funktionella egenskaper skapas i konstruktören, vilket innebär att du återigen kan tilldela standardvärden till egenskaper via konstruktörens parametrar. Eftersom endast en instans av showColor() -funktionen skapas, finns det ingen minnesförlust. Dessutom påverkar inte tillägget av "Bill" till oCar1:s drivers-array oCar2:s array, så när du skriver ut värdena för dessa arrayer visar oCar1.drivers "Mike,John,Bill", medan oCar2.drivers visar "Mike,John". Eftersom vi använder prototypmetoden kan vi fortfarande använda instanceof-operatören för att avgöra objektets typ.

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 feels less 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 has well-packaged all properties and methods of the Car class, so seeing this code knows 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 within the constructor and methods outside of it. Therefore, they designed dynamic prototype methods to provide a more user-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

Fram till att typeof Car._initialized är lika med "undefined" har denna konstruktionsfunktion inte ändrats. Detta är den viktigaste delen i dynamiska ursprungsmetoder. Om detta värde inte är definierat kommer konstruktionsfunktionen att fortsätta definiera objektets metoder på prototypsättet och sedan sätta Car._initialized till true. Om detta värde är definierat ( dess värde är true när typeof-värdet är Boolean), kommer metoden inte att skapas. För att sammanfatta använder denna metod ett flagga (_initialized) för att avgöra om några metoder har tilldelats prototypen. Metoden skapas och tilldelas endast en gång, och traditionella OOP-utvecklare kommer att glädjas över att detta kodsektion ser mycket lik en klassdefinition i andra språk.

Blandad fabrikmetod

Detta sätt används vanligtvis som en kompromiss när det inte kan användas på det föregående sättet. Syftet är att skapa en falsk konstruktionsfunktion som bara returnerar en ny instans av ett annat objekt.

Denna kod ser mycket lik en fabrikffunktion:

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

TIY

Skillnaden från det klassiska sättet är att detta sätt använder new-operatorn, vilket gör det att se ut som en riktig konstruktionsfunktion:

var car = new Car();

Eftersom new-operatorn anropas inom Car() konstruktionsfunktionen, kommer den andra new-operatorn (utanför konstruktionsfunktionen) att ignoreras, och det objekt som skapas inom konstruktionsfunktionen överförs till variabeln car.

Detta sätt har samma problem som det klassiska sättet när det gäller intern hantering av objektmetoder. Det rekommenderas starkt: undvik detta sätt om det inte är nödvändigt.

Vilket sätt att använda

Som nämnts tidigare är det mest använda sättet att blanda konstruktionsfunktioner och prototyper. Dessutom är dynamiska ursprungsmetoder mycket populära och fungerar funktionsmässigt på samma sätt som konstruktionsfunktioner och prototyper. Du kan använda vilket av dessa sätt som helst. Men undvik att använda klassiska konstruktionsfunktioner eller prototyper ensamma, eftersom detta kan introducera problem i koden.

Exempel

Ett intressant sätt att använda objekt är deras sätt att lösa problem. Ett av de mest vanliga problemen i ECMAScript är prestandan för strängkombination. Som med andra språk är strängar i ECMAScript oförändrade, vilket innebär att deras värden inte kan ändras. Låt oss titta på följande kod:

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

Faktiskt utför detta kodexempel följande steg i bakgrunden:

  1. Skapa en sträng som lagrar "hello ";
  2. Skapa en sträng som lagrar "world".
  3. Skapa en sträng som lagrar den anslutna resultatet.
  4. Kopiera strings aktuella innehåll till resultatet.
  5. Kopiera "world" till resultatet.
  6. Uppdatera str så att den pekar på resultatet.

Varje gång en stränganslutning utförs körs steg 2 till 6, vilket gör denna operation mycket resurskrävande. Om du upprepar detta flera hundra eller till och med tusen gånger, kan det orsaka prestandaproblem. Lösningen är att lagra strängarna i ett Array-objekt och sedan skapa den slutliga strängen med join() metoden (med ett tomt tecken som parameter). Tänk dig att ersätta det tidigare koden med följande:

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

Så, det spelar ingen roll hur många strängar som läggs till i arrayen, eftersom anslutningsoperationen bara sker när join() metoden anropas. Då utförs följande steg:

  1. Skapa en sträng som lagrar resultatet
  2. Kopiera varje sträng till lämplig plats i resultatet

Även om detta är ett bra lösning, finns det ännu bättre sätt. Problemet är att detta kodexempel inte exakt reflekterar dess intention. För att göra det enklare att förstå kan du paketera denna funktion med StringBuffer-klassen:

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

Det första som bör noteras i detta kodexempel är strings-attributet, som är ett privat attribut. Det har bara två metoder, nämligen append() och toString() metoden. append() metoden har en parameter som lägger till detta parameter till en strängarray, toString() metoden anropar arrayns join-metod och returnerar den faktiskt anslutna strängen. För att använda StringBuffer-objektet för att ansluta en uppsättning strängar kan du använda följande kod:

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

TIY

Du kan testa prestandan hos StringBuffer-objektet och den traditionella strängkombinationsmetoden med följande kod:

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

Detta kodexempel genomför två tester för strängkombination, den ena använder plus-tecknet och den andra använder StringBuffer-klassen. Varje operation kombinerar 10000 strängar. Datumvärdena d1 och d2 används för att bedöma hur lång tid det tar att slutföra operationen.Observera att om inga parametrar anges när ett Date-objekt skapas, tilldelas objektet nuvarande datum och tid. För att beräkna hur lång tid det tar att slutföra kombinationsoperationen, subtrahera millisekundvärdet (returnerat av getTime()-metoden). Detta är en vanlig metod för att mäta JavaScript:s prestanda. Resultatet av testet kan hjälpa dig att jämföra effektiviteten mellan att använda StringBuffer-klassen och att använda plus-tecknet.