Define Classes or Objects in ECMAScript

Het gebruik van vooraf gedefinieerde objecten is slechts een deel van de capaciteiten van een objectgeoriënteerde taal, zijn werkelijke kracht ligt in het kunnen maken van eigen specifieke klassen en objecten.

ECMAScript heeft veel methoden om objecten of klassen te maken.

Factorie manier

Oorspronkelijke manier

Omdat de eigenschappen van het object na het maken van het object dynamisch kunnen worden gedefinieerd, schrijven veel ontwikkelaars bij het eerste introduceren van JavaScript code zoals hieronder:

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

TIY

In de bovenstaande code wordt een object car gemaakt. Vervolgens worden enkele eigenschappen toegekend: de kleur is blauw, er zijn vier deuren, en elke gallon kan 25 mijl rijden. De laatste eigenschap is een verwijzing naar een functie, wat betekent dat het een methode is. Na het uitvoeren van dit stuk code kan het object car worden gebruikt.

Maar er is hier een probleem, dat is dat het mogelijk is om meerdere car-exemplaren te maken.

Oplossing: fabrieksmanier

Om dit probleem op te lossen, hebben ontwikkelaars fabrieksfuncties bedacht die specifieke types objecten kunnen maken en retourneren.

Bijvoorbeeld, de functie createCar() kan worden gebruikt om de acties voor het maken van car-objecten die eerder zijn vermeld te verpakken:

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

Hier bevinden zich alle codes van het eerste voorbeeld binnen de functie createCar(). Daarnaast is er een extra regel code, die het car-object (oTempCar) als functiewaarde retourneert. Door deze functie aan te roepen, wordt een nieuw object gemaakt en wordt het alle benodigde eigenschappen gegeven, waardoor een car-object wordt gekopieerd zoals we dat eerder hebben beschreven. Op deze manier kunnen we eenvoudig twee versies van car-objecten (oCar1 en oCar2) maken, die dezelfde eigenschappen hebben.

Geef parameters door aan de functie

We kunnen de functie createCar() ook aanpassen door de standaardwaarden van de verschillende eigenschappen door te geven, niet alleen eenvoudig de standaardwaarden van de eigenschappen toe te wijzen:

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

TIY

Voeg parameters toe aan de functie createCar(), zodat je de color, doors en mpg-eigenschappen van het te maken car-object kunt toewijzen. Dit maakt twee objecten met dezelfde eigenschappen, maar met verschillende eigenschapswaarden.

Definieer objectmethoden buiten de fabrieksfunctie

Hoewel ECMAScript steeds formeler wordt, worden de methoden om objecten te maken genegeerd en wordt hun standaardisatie nog steeds verward. Een deel is van semantische redenen (het ziet er niet uit als het gebruik van de constructor new-operand), een deel is van functionele redenen. De functionele reden ligt in het moeten maken van objecten op deze manier. In de vorige voorbeelden moet elke oproep van de functie createCar() een nieuwe functie showColor() maken, wat betekent dat elke object zijn eigen versie van showColor() heeft. Terwijl in feite elke object dezelfde functie deelt.

Sommige ontwikkelaars definiëren de methoden van het object buiten de fabrieksfunctie en wijzen deze vervolgens door middel van eigenschappen naar deze methode om dit probleem te vermijden:

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

TIY

In de hierboven herschreven code is de functie showColor() voor de functie createCar() gedefinieerd. Binnen createCar() wordt er een verwijzing naar de bestaande functie showColor() toegewezen aan het object. Functioneel gezien lost dit het probleem van het herhaaldelijk maken van functieobjecten op; maar semantisch gezien lijkt deze functie niet echt als een objectmethode.

Al deze vragen hebben geleid totontwikkelaarsdefinitiehet verschijnen van de constructor.

Constructiefunctie

Het maken van een constructor is net zo eenvoudig als het maken van een fabrieksfunctie. De eerste stap is het kiezen van de naam van de klasse, dat wil zeggen de naam van de constructor. Volgens de traditie wordt de eerste letter van deze naam groter gemaakt om hem te onderscheiden van variabelen met een kleine letter. Behalve dit verschil lijkt de constructor erg op een fabrieksfunctie. Overweeg het volgende voorbeeld:

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

Hier wordt u uitgelegd wat het verschil is tussen de code en de fabrieksmodus. Eerst wordt er in de constructor geen object gemaakt, maar wordt de this-sleutel gebruikt. Bij het gebruik van de constructor met de new-betekenis wordt er eerst een object gemaakt voordat de eerste regel code wordt uitgevoerd, alleen met this kan het object worden bereikt. Vervolgens kan de this-eigenschap direct worden toegewezen, standaard is dit de returnwaarde van de constructor (er hoeft geen expliciete return-betekenis te worden gebruikt).

Nu lijkt het creëren van objecten met de new-bewerker en de classnaam Car veel meer op het creëren van algemene objecten in ECMAScript.

Je zou kunnen vragen of er in deze manier dezelfde problemen met het beheren van functies zijn als in de vorige manier? Ja.

Net zoals een factory-functie, maakt de constructor herhaaldelijk functies, waarbij voor elk object een独立的 functieversie wordt gecreëerd. Hoewel dit vergelijkbaar is met de factory-functie, kan de constructor ook worden herschreven met een externe functie, wat semantisch geen betekenis heeft. Dit is precies de voordeel van de prototype-methode die we straks zullen bespreken.

Prototype-methode

Deze manier maakt gebruik van de prototype-eigenschap van objecten en kan worden gezien als het prototype dat nodig is om nieuwe objecten te maken.

Hier wordt eerst een lege constructor gebruikt om de classnaam in te stellen. Vervolgens worden alle eigenschappen en methoden direct toegewezen aan het prototype-eigenschap. We herschrijven het vorige voorbeeld, de code ziet er als volgt uit:

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

In dit stuk code wordt eerst een constructor (Car) gedefinieerd, zonder enige code. De volgende regels voegen eigenschappen toe aan het prototype-eigenschap van Car om de eigenschappen van het Car-object te definiëren. Bij het oproepen van new Car(), worden alle eigenschappen van het prototype onmiddellijk toegewezen aan het te creëren object, wat betekent dat alle Car-instanties pointers naar de showColor()-functie bevatten. Semantisch gezien lijken alle eigenschappen toe te behoren aan een object, waardoor de problemen van de vorige twee methoden worden opgelost.

Daarnaast kan je met deze manier ook de instanceof-bewerker gebruiken om het type van het door een variabele verwijzende object te controleren. Daarom zal het volgende codefragment TRUE uitvoeren:

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

Problemen van de prototype-methode

De prototype-methode lijkt een goede oplossing te zijn. Helaas is dat niet het geval.

Eerst en vooral, deze constructor heeft geen parameters. Omdat de prototype-methode niet toestaat dat je parameters naar de constructor stuurt om de waarden van de eigenschappen te initialiseren, omdat de color-eigenschap van Car1 en Car2 allemaal "blue" is, de doors-eigenschap allemaal 4 is en de mpg-eigenschap allemaal 25 is, betekent dit dat je de standaardwaarden van de eigenschappen alleen na het aanmaken van het object kunt wijzigen, wat zeer vervelend is, maar het is nog niet alles. Het echte probleem zit in het feit dat de eigenschappen naar objecten wijzen in plaats van naar functies. Functies delen veroorzaakt geen probleem, maar objecten worden zelden gedeeld door meerdere instanties. Overweeg de volgende voorbeeld:

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

TIY

In de bovenstaande code is de eigenschap drivers een verwijzing naar een Array-object dat twee namen "Mike" en "John" bevat. Omdat drivers een referentiewaarde is, wijzen beide instanties van Car naar dezelfde array. Dit betekent dat het toevoegen van de waarde "Bill" aan oCar1.drivers ook in oCar2.drivers zichtbaar is. De uitvoer van een van deze verwijzingen is de string "Mike,John,Bill".

Vanwege al deze vragen die bij het creëren van objecten opkomen, zou je waarschijnlijk willen weten of er een redelijke methode is om objecten te creëren. Het antwoord is ja, je moet de constructor en de prototype-methode combineren.

Gemengde constructor/prototype-methode

De combinatie van constructor en prototype-methode maakt het mogelijk om objecten te creëren zoals in andere programmeertalen. Dit concept is zeer eenvoudig, namelijk het definiëren van alle niet-functie-eigenschappen van een object via de constructor, en de functie-eigenschappen (methoden) van het object via de prototype-methode definiëren. Het resultaat is dat alle functies slechts eenmaal worden gecreëerd, en elk object heeft zijn eigen instantie van objecteigenschappen.

We hebben de vorige voorbeeldherhaling herschreven, de code ziet er als volgt uit:

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

TIY

Nu is het meer alsof je een algemeen object creëert. Alle niet-functie-eigenschappen worden in de constructor gecreëerd, wat betekent dat je opnieuw standaardwaarden kunt toekennen aan eigenschappen via de parameters van de constructor. Omdat alleen een enkel exemplaar van de functie showColor() wordt gecreëerd, is er geen geheugenverspilling. Bovendien heeft het toevoegen van de waarde "Bill" aan de array drivers van oCar1 geen invloed op de array van oCar2, dus wanneer de waarden van deze arrays worden weergegeven, toont oCar1.drivers "Mike,John,Bill", terwijl oCar2.drivers "Mike,John" toont. Omdat we de prototype-methode gebruiken, kunnen we nog steeds het instanceof-beschikbare operator gebruiken om het type van een object te bepalen.

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 Method

For developers accustomed to using other languages, using the mixed constructor/proto pattern may not feel as 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 properties and methods of the Car class well, so seeing this code knows what it is supposed to do, it defines an object's information. Critics of the mixed constructor/proto pattern argue that it is illogical to find properties within the constructor and methods outside of it. Therefore, they designed the dynamic prototype method to provide a more user-friendly coding style.

The basic idea of the dynamic prototype method is similar to the mixed constructor/proto pattern, that is, defining non-function properties within the constructor, while function properties are defined using prototype properties. The only difference is the position at which methods are assigned to the object. The following is the Car class rewritten using the dynamic prototype method:

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

Totdat gecontroleerd wordt of typeof Car._initialized gelijk is aan "undefined", blijft deze constructor onveranderd. Dit is het belangrijkste deel van de dynamische protype methode. Als deze waarde niet is gedefinieerd, zal de constructor de methoden van het object blijven definiëren met de protype manier en Car._initialized instellen op true. Als deze waarde is gedefinieerd (zijn waarde is true, de waarde van typeof is Boolean), zal deze methode niet worden gecreëerd. Kortom, deze methode gebruikt een signaal (_initialized) om te bepalen of er al methoden zijn toegevoegd aan het prototype. Deze methode wordt slechts eenmaal gecreëerd en toegewezen, en traditionele OOP ontwikkelaars zullen blij zijn dat dit codevoorbeeld meer lijkt op classdefinities in andere talen.

Gemengde factory manier

Deze manier is meestal een alternatieve oplossing wanneer de vorige manier niet kan worden toegepast. Het doel is om een pseudo constructor te maken die alleen een nieuwe instantie van een ander object retourneert.

Dit codevoorbeeld lijkt sterk op een factory functie:

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

TIY

Verschillend van de klassieke manier, gebruikt deze manier de new operator, waardoor het eruitziet als een echte constructor:

var car = new Car();

Omdat de new operator binnen de Car() constructor wordt aangeroepen, wordt de tweede new operator (buiten de constructor) genegeerd en het object dat binnen de constructor wordt gemaakt, wordt doorgegeven aan de variabele car.

Deze manier heeft dezelfde problemen met de interne beheer van objectmethoden als de klassieke manier. Het wordt sterk aanbevolen: vermijd deze manier tenzij absoluut noodzakelijk.

Welke manier kiezen

Zoals eerder vermeld, is het momenteel het meest gebruikte een gemengde constructor/prootype manier. Daarnaast zijn dynamische primitive methoden ook zeer populair en equivalente in functie tot de constructor/prootype manier. U kunt een van beide manieren kiezen. Gebruik echter niet alleen de klassieke constructor of protype manier, omdat dit problemen kan introduceren in de code.

Voorbeeld

Een interessant aspect van objecten is de manier waarop ze problemen oplossen. Een van de meest voorkomende problemen in ECMAScript is de prestatie van stringkoppeling. Net als in andere talen zijn strings in ECMAScript onveranderlijk, wat betekent dat hun waarde niet kan worden gewijzigd. Overweeg het volgende codevoorbeeld:

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

In feite voert dit code achter de schermen de volgende stappen uit:

  1. Maak een string aan om "hello " op te slaan.
  2. Maak een string aan om "world" op te slaan.
  3. Maak een string aan om de verbonden resultaten op te slaan.
  4. Kopieer de huidige inhoud van str naar het resultaat.
  5. Kopieer "world" naar het resultaat.
  6. Update str, zodat het naar het resultaat wijst.

Elke keer dat een string wordt gekoppeld, worden stappen 2 tot 6 uitgevoerd, waardoor deze operatie zeer hoge ressourcegebruik veroorzaakt. Als je dit proces honderden of zelfs duizenden keren herhaalt, kan dit prestatieproblemen veroorzaken. De oplossing is om strings op te slaan in een Array-object en vervolgens de join() methode (met een lege string als parameter) te gebruiken om de uiteindelijke string te maken. Stel je voor om de volgende code te vervangen door de eerdere code:

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

Op deze manier is het geen probleem om zoveel strings als je wilt in het array op te nemen, omdat de koppeling alleen plaatsvindt wanneer de join() methode wordt aangeroepen. De uitgevoerde stappen zijn als volgt:

  1. Maak een string aan om het resultaat op te slaan
  2. Kopieer elke string naar de juiste positie in het resultaat

Hoewel deze oplossing goed is, is er een betere manier. Het probleem is dat dit code niet exact zijn intentie weergeeft. Om het beter begrijpelijk te maken, kan je deze functionaliteit met de StringBuffer-klasse verpakken:

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

De eerste aandachtspunt bij dit code is het strings-eigenschap, die oorspronkelijk een privaat eigenschap is. Het heeft slechts twee methoden, namelijk de append() en toString() methoden. De append() methode heeft een parameter, die deze parameter aan het string-array toevoegt, en de toString() methode roept de join-methode van het array aan om de werkelijk verbonden string terug te keren. Om een groep strings te koppelen met een StringBuffer-object, kunt u de volgende code gebruiken:

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

TIY

Je kunt de prestaties van de StringBuffer-object en de traditionele stringkoppelingmethode testen met behulp van de onderstaande code:

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

TIY

Deze code voert twee tests uit op stringkoppeling, de eerste met het plus-teken en de tweede met de StringBuffer-klasse. Elke operatie koppelt 10000 strings aan elkaar. De datumwaarden d1 en d2 worden gebruikt om de tijd te bepalen die nodig is voor de operatie. Let op, als er geen argumenten worden gegeven bij het aanmaken van een Date-object, wordt de huidige datum en tijd aan het object toegewezen. Om de tijd te berekenen die nodig is voor de koppeling, subtracteer je de millisecondwaarden van de datums (de returnwaarde van de getTime()-methode). Dit is een veelvoorkomende methode om de prestaties van JavaScript te meten. Het resultaat van deze test kan je helpen om de efficiëntieverschillen tussen het gebruik van de StringBuffer-klasse en het gebruik van het plus-teken te vergelijken.