Implementering av ECMAScript-ärvningssystem

Implementering av arvsmekanismen

För att implementera arvsmekanismen med ECMAScript kan du börja med den basklass du vill arbeta med. Alla klasser definierade av utvecklaren kan användas som basklass. Av säkerhetsskäl kan inte lokala klasser och värdklasser användas som basklasser, vilket förhindrar allmän åtkomst till compilerade kodnivåer i webbläsare, eftersom denna kod kan användas för illa.

Efter att ha valt en basklass kan du skapa dess underklass. Om du använder basklassen beror helt på dig själv. Ibland kan du vilja skapa en basklass som inte kan användas direkt, utan bara används för att ge underklassen allmänna funktioner. I detta fall betraktas basklassen som en abstrakt klass.

Trots att ECMAScript inte definierar abstrakta klasser lika strikt som andra språk, skapar det ibland några klasser som inte är tillåtna att använda. Vanligtvis kallar vi sådana klasser för abstrakta klasser.

Den skapade underklassen kommer att ärva alla egenskaper och metoder från överklassen, inklusive konstruktorn och implementeringen av metoderna. Kom ihåg att alla egenskaper och metoder är gemensamma, så underklassen kan direkt komma åt dessa metoder. Underklassen kan också lägga till nya egenskaper och metoder som inte finns i överklassen, och kan också överskriva egenskaper och metoder från överklassen.

Sätt att arbeta vidare

Som med andra funktioner har ECMAScript flera sätt att implementera arv. Detta beror på att arvsmekanismen i JavaScript inte är tydligt definierad, utan genom att imitera. Detta innebär att alla detaljer i arvet inte hanteras helt av tolkningen. Som utvecklare har du rätt att välja det mest lämpliga sättet att arbeta vidare.

Nedan presenteras några specifika sätt att arbeta vidare.

Object masquerading

När man tänkte ut ECMAScript, var det aldrig meningen att designa object masquerading. Det utvecklades först när utvecklarna började förstå hur funktioner fungerar, särskilt hur this-nyckelordet används i funktionsmiljöer.

Principen är som följer: Konstruktorn använder this-nyckelordet för att tilldela alla egenskaper och metoder (dvs. i enlighet med klassdeklarationens konstruktör). Eftersom konstruktorn bara är en funktion, kan ClassA-konstruktorn bli en metod för ClassB, och sedan anropas. ClassB kommer att få tillgång till egenskaper och metoder definierade i ClassA-konstruktorn. Till exempel kan ClassA och ClassB definieras på följande sätt:

function ClassA(sColor) {
    this.color = sColor;
    this.sayColor = function () {
        alert(this.color);
    }
}
function ClassB(sColor) {
}

Kom ihåg? Nyckelordet this refererar till den aktuella objektet som skapas av konstruktorn. Men i denna metod pekar this på det objekt som tillhör. Principen är att använda ClassA som en vanlig funktion för att skapa ett arvssystem, inte som en konstruktör. Här är hur man kan använda konstruktorn ClassB för att implementera ett arvssystem:

function ClassB(sColor) {
    this.newMethod = ClassA;
    this.newMethod(sColor);
    delete this.newMethod;
}

I detta kodsegment har metoden newMethod tilldelats till ClassA (kom ihåg att funktionsnamnet bara är en pekare till den). Därefter anropas denna metod och den parameter som överlämnas är sColor från konstruktorn till ClassB. Sista raden i koden tar bort referensen till ClassA, så att den inte kan anropas igen.

Alla nya egenskaper och metoder måste definieras efter att den nya metoden har tagits bort från koden. Annars kan det hända att överklassens relaterade egenskaper och metoder överskrivs:

function ClassB(sColor, sName) {
    this.newMethod = ClassA;
    this.newMethod(sColor);
    delete this.newMethod;
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    }
}

För att bevisa att den tidigare koden fungerar, kan följande exempel köras:

var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor(); // Utmatar "blue"
objB.sayColor(); // Utmatar "red"
objB.sayName();		// Output "John"

Object masquerading kan implementera flera arv

Det är intressant att object masquerading stöder flera arv. Detta innebär att en klass kan arbeta vidare från flera överklasser. En flera arvs mekanism som visas med UML är som följer:

Arvssystem UML-diagramsexempel

Till exempel, om det finns två klasser ClassX och ClassY, och ClassZ vill arbeta vidare från dessa klasser, kan följande kod användas:

function ClassZ() {
    this.newMethod = ClassX;
    this.newMethod();
    delete this.newMethod;
    this.newMethod = ClassY;
    this.newMethod();
    delete this.newMethod;
}

TIY

Det finns ett nackdel här, om det finns två klasser ClassX och ClassY som har同名属性或方法,ClassY har hög prioritet eftersom den ärver från den senare klassen. Förutom detta lilla problem är det enkelt att implementera en flerfaldig arvsmekanism med objektsutväxling.

På grund av populariteten hos detta arvsmekanism, lade ECMAScript:s tredje version till två metoder till Function-objektet, nämligen call() och apply().

call() metoden

call() metoden är mest lik den klassiska objektsutväxlingsmetoden. Den första parametern används som objekt för this. De andra parametrarna överförs direkt till funktionen själv. Till exempel:

function sayColor(sPrefix,sSuffix) {
    alert(sPrefix + this.color + sSuffix);
}
var obj = new Object();
obj.color = "blue";
sayColor.call(obj, "The color is ", "a very nice color indeed.");

I detta exempel är funktionen sayColor() definierad utanför objektet, även om den inte tillhör något objekt, kan vi använda nyckelordet this. Objektets color-attribut är lika med blue. När call() metoden anropas är den första parametern obj, vilket innebär att värdet för this i sayColor() funktionen ska vara obj. De andra två parametrarna är strängar. De matchar parametrarna sPrefix och sSuffix i sayColor() funktionen, och den slutliga meddelandet "The color is blue, a very nice color indeed." kommer att visas.

För att använda metoden tillsammans med objektsutväxlingens metod för arv, byt endast ut de tre första raderna med tilldelning, anrop och radering av kod:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.call(this, sColor);
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    }
}

TIY

Här behöver vi att ClassA:s nyckelord this är lika med den nyss skapade ClassB-objektet, så this är den första parametern. Den andra parametern sColor är unik för båda klasserna.

apply() metoden

apply() metoden har två parametrar, som används som objekt för this och en array av parametrar som ska överföras till funktionen. Till exempel:

function sayColor(sPrefix,sSuffix) {
    alert(sPrefix + this.color + sSuffix);
}
var obj = new Object();
obj.color = "blue";
sayColor.apply(obj, new Array("The color is ", "a very nice color indeed."));

Detta exempel är lika som det tidigare exemplet, men nu anropas apply() -metoden. När man anropar apply() -metoden är den första parametern fortfarande obj, vilket innebär att this-kärnan för sayColor() -funktionen bör vara obj. Den andra parametern är en array av två strängar som matchar parametrarna sPrefix och sSuffix i sayColor() -funktionen, och den slutliga meddelandet är fortfarande "The color is blue, a very nice color indeed." som kommer att visas.

Denna metod används också för att ersätta de tre första raderna med tilldelning, anrop och radering av den nya metoden:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.apply(this, new Array(sColor));
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    }
}

På samma sätt är den första parametern fortfarande this, och den andra parametern är en array med ett enda värde color. Man kan överföra hela arguments-objektet från ClassB som den andra parametern till apply() -metoden:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.apply(this, arguments);
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    }
}

TIY

För att kunna överföra parametarobjektet måste naturligtvis parameterordningen i superklassen vara helt identisk med den i underklassen. Om detta inte är fallet måste man skapa en separat array där parametrarna placeras i korrekt ordning. Dessutom kan man använda call() -metoden.

Prototypkedja (prototype chaining)

Denna form av arv används ursprungligen i ECMAScript för prototypkedjan. I föregående kapitel introducerades sättet att definiera klasser genom prototyp. Prototypkedjan utökar detta sätt på ett intressant sätt för att implementera arvsmekanismen.

Som vi lärde oss i föregående kapitel, är prototype-objektet ett mönster som alla att instansiera objekt baseras på. Sammanfattningsvis överförs alla egenskaper och metoder på prototype-objektet till alla instanser av den här klassen. Prototypkedjan använder denna funktion för att implementera arvsmekanismen.

Om man definierar klassen på prototypbasis igen, kommer den att bli följande form:

function ClassA() {
}
ClassA.prototype.color = "blue";
ClassA.prototype.sayColor = function () {
    alert(this.color);
}
function ClassB() {
}
ClassB.prototype = new ClassA();

Det fantastiska med prototypmetoden är det markerade blå kodraden. Här sätts ClassB:s prototype-attribut till en instans av ClassA. Det är mycket intressant, eftersom man vill ha alla egenskaper och metoder från ClassA, men inte vill lägga till dem en efter en till ClassB:s prototype-attribut. Finns det något bättre sätt att tilldela en instans av ClassA till prototype-attributet?

Observera:Anropa ClassA:s konstruktör utan att ge det några parametrar. Detta är en standardpraktik i prototypkedjan. Se till att konstruktören inte har några parametrar.

Liknande objektsmaskering måste alla egenskaper och metoder för en underklass finnas i prototype-attributet efter att det har tilldelats, eftersom alla metoder som tilldelas innan det kommer att tas bort. Varför? Eftersom prototype-attributet ersätts av ett nytt objekt, och det ursprungliga objektet som lade till metoderna kommer att förstöras. Så, här är koden för att lägga till name-attributet och sayName()-metoden för ClassB-klassen:

function ClassB() {
}
ClassB.prototype = new ClassA();
ClassB.prototype.name = "";
ClassB.prototype.sayName = function () {
    alert(this.name);
}

Man kan testa detta kodexempel genom att köra följande:

var objA = new ClassA();
var objB = new ClassB();
objA.color = "blue";
objB.color = "red";
objB.name = "John";
objA.sayColor();
objB.sayColor();
objB.sayName();

TIY

Dessutom fungerar instanceof-operatorn på ett mycket unikt sätt i prototypkedjan. För alla instanser av ClassB returnerar instanceof både ClassA och ClassB true. Till exempel:

var objB = new ClassB();
alert(objB instanceof ClassA);	// Output "true"
alert(objB instanceof ClassB);	// Output "true"

I ECMAScript:s svaga typvärld är detta ett mycket användbart verktyg, men det kan inte användas när man använder objektsmaskering.

En nackdel med prototypkedjan är att den inte stöder flera arv. Kom ihåg, prototypkedjan kommer att skriva över klassens prototype-attribut med en annan typ av objekt.

Blandad metod

Detta arvsätt använder konstruktörfunktioner för att definiera klasser, inte några prototyper. Det huvudsakliga problemet med att använda objektsmaskering är att man måste använda konstruktörfunktioner, vilket inte är det bästa valet. Men om man använder prototypkedjan, kan man inte använda konstruktörer med parametrar. Hur väljer utvecklaren? Svar: mycket enkelt, båda.

I föregående kapitel har vi redan förklarat att det bästa sättet att skapa en klass är att definiera egenskaper med konstruktörer och metoder med prototyp. Detta sätt är också lämpligt för arvsmekanismen, där egenskaper från konstruktören ersätts med objekt och metoder från prototype-objektet implementeras genom prototypkedjan. Genom att använda dessa två sätt kan föregående exempel omformuleras som följer:

function ClassA(sColor) {
    this.color = sColor;
}
ClassA.prototype.sayColor = function () {
    alert(this.color);
}
function ClassB(sColor, sName) {
    ClassA.call(this, sColor);
    this.name = sName;
}
ClassB.prototype = new ClassA();
ClassB.prototype.sayName = function () {
    alert(this.name);
}

I detta exempel implementeras arvsmekanismen av de två markerade blå kodraderna. I den första markerade raden, i konstruktören för ClassB, ersätts ClassA-klassens sColor-attribut med ett objekt. I den andra markerade raden implementeras ClassA-klassens metoder genom att använda prototypkedjan. Eftersom detta blandade sätt använder prototypkedjan fungerar instanceof-operatören korrekt.

Följande exempel testar detta kodblock:

var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor(); // Utmatar "blue"
objB.sayColor(); // Utmatar "red"
objB.sayName(); // Utmatar "John"

TIY