Implementation of Inheritance Mechanism in ECMAScript

Implementatie van het inhertiemechanisme

Om de inhertiemechanismen van ECMAScript te implementeren, kun je beginnen met de basisklasse die je wilt overnemen. Alle door ontwikkelaars gedefinieerde klassen kunnen als basisklasse dienen. Vanwege veiligheidsredenen kunnen lokale klassen en hostklassen niet als basisklasse dienen, zodat de toegang tot gecompileerde browsercode wordt voorkomen, die kan worden gebruikt voor kwaadaardige aanvallen.

Na het kiezen van de basisklasse kun je de subklasse ervan maken. Of je de basisklasse gebruikt, is geheel aan jou. Soms kun je een basisklasse maken die niet direct kan worden gebruikt, maar alleen om algemene functies voor subklassen te bieden. In dit geval wordt de basisklasse gezien als een abstracte klasse.

Hoewel ECMAScript niet zo strikt abstracte klassen definieert als andere talen, maakt het soms wel klassen die niet gebruikt mogen worden. Dergelijke klassen noemen we meestal abstracte klassen.

De gemaakte subklasse zal alle eigenschappen en methoden van de superklasse overnemen, inclusief de implementatie van de constructor en methoden. Onthoud dat alle eigenschappen en methoden publiek zijn, dus kan de subklasse deze methoden direct benaderen. De subklasse kan ook nieuwe eigenschappen en methoden toevoegen die niet in de superklasse voorkomen, of de eigenschappen en methoden van de superklasse overschrijven.

Manieren van erfrechtsverwerving

Net als andere functies, zijn er meerdere manieren om erfrechtsverwerving in ECMAScript te implementeren. Dit komt omdat het erfrechtsverwervingssysteem in JavaScript niet strikt gedefinieerd is, maar door imitatie wordt gerealiseerd. Dit betekent dat alle details van de erfrechtsverwerving niet volledig door de interpreter worden afgehandeld. Als ontwikkelaar heb je het recht om de meest geschikte manier van erfrechtsverwerving te kiezen.

Hieronder worden enkele specifieke manieren van erfrechtsverwerving besproken.

Objectvermomming

Bij het ontwerpen van het oorspronkelijke ECMAScript was er geen plan om objectvermomming (object masquerading) te ontwerpen. Het is ontwikkeld nadat ontwikkelaars begonnen zijn te begrijpen hoe functies werken, vooral hoe het sleutelwoord this wordt gebruikt in de functieomgeving.

Het principe ervan is als volgt: de constructor gebruikt het sleutelwoord this om alle eigenschappen en methoden toe te wijzen (dus zoals de constructor van de klasse wordt gedeclareerd). Omdat de constructor een functie is, kan de constructor van ClassA worden gebruikt als een methode van ClassB en wordt aangeroepen. ClassB ontvangt dan de eigenschappen en methoden die in de constructor van ClassA zijn gedefinieerd. Bijvoorbeeld, ClassA en ClassB kunnen worden gedefinieerd op de volgende manier:

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

Herinner je je nog? De sleutelwoord this verwijst naar het object dat de constructor momenteel aanmaakt. Maar in deze methode verwijst this naar het eigendom van het object. Dit principe is om ClassA als een reguliere functie te gebruiken om het erfrechtsverwervingssysteem op te bouwen, in plaats van als een constructor. De erfrechtsverwervingssysteem kan worden gerealiseerd met de constructor ClassB zoals volgt:

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

In deze code wordt de methode newMethod van ClassA toegewezen (onthoud dat de functienaam enkel een verwijzing naar hetzelfde is). Vervolgens wordt deze methode aangeroepen, waarbij de parameters van de constructor van ClassB worden doorgegeven. De laatste regel code verwijdert de verwijzing naar ClassA, zodat deze in de toekomst niet meer kan worden aangeroepen.

Alle nieuwe eigenschappen en methoden moeten worden gedefinieerd na het verwijderen van de nieuwe methode. Anders kan het mogelijk overeenkomende eigenschappen en methoden van de superklasse overschrijven:

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

Om te bewijzen dat de vorige code werkt, kan je het volgende voorbeeld uitvoeren:

var objA = new ClassA("blauw");
var objB = new ClassB("rood", "John");
objA.sayColor(); // Uitvoer "blauw"
objB.sayColor(); // Uitvoer "rood"
objB.sayName(); // Uitvoer "John"

Objectvermomming kan meervoudige erfrechtsverwerving realiseren

Het is interessant dat objectvermomming meervoudige erfrechtsverwerving ondersteunt. Dit betekent dat een klasse meerdere superklassen kan erven. Het meervoudige erfrechtsverwervingssysteem weergegeven met UML is zoals in het volgende figuur:

Inheritance Mechanism UML Diagram Example

Bijvoorbeeld, als er twee klassen ClassX en ClassY zijn, kan ClassZ deze klassen erven door het volgende code te gebruiken:

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

TIY

这里存在一个弊端,如果存在两个类 ClassX 和 ClassY 具有同名的属性或方法,ClassY 具有高优先级。因为它从后面的类继承。除这点小问题之外,用对象冒充实现多重继承机制轻而易举。

由于这种继承方法的流行,ECMAScript 的第三版为 Function 对象加入了两个方法,即 call() 和 apply()。

call() 方法

call() 方法是与经典的对象冒充方法最相似的方法。它的第一个参数用作 this 的对象。其他参数都直接传递给函数自身。例如:

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

在这个例子中,函数 sayColor() 在对象外定义,即使它不属于任何对象,也可以引用关键字 this。对象 obj 的 color 属性等于 blue。调用 call() 方法时,第一个参数是 obj,说明应该赋予 sayColor() 函数中的 this 关键字值是 obj。第二个和第三个参数是字符串。它们与 sayColor() 函数中的参数 sPrefix 和 sSuffix 匹配,最后生成的消息 "The color is blue, a very nice color indeed." 将被显示出来。

要与继承机制的对象冒充方法一起使用该方法,只需将前三行的赋值、调用和删除代码替换即可:

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

这里,我们需要让 ClassA 中的关键字 this 等于新创建的 ClassB 对象,因此 this 是第一个参数。第二个参数 sColor 对两个类来说都是唯一的参数。

apply() 方法

apply() 方法有两个参数,用作 this 的对象和要传递给函数的参数的数组。例如:

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

Dit voorbeeld is hetzelfde als het vorige voorbeeld, alleen nu wordt de apply() methode aangeroepen. Bij het aanroepen van de apply() methode is de eerste parameter nog steeds obj, wat betekent dat de waarde van de this-sleutel in de sayColor() functie obj moet zijn. De tweede parameter is een array met twee strings die overeenkomen met de parameters sPrefix en sSuffix in de sayColor() functie, en het uiteindelijke bericht is nog steeds "The color is blue, a very nice color indeed." en zal worden weergegeven.

Deze methode wordt ook gebruikt om de code van de toewijzing, aanroep en verwijdering van de nieuwe methode in de drie regels te vervangen:

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

Op dezelfde manier is de eerste parameter nog steeds this, en de tweede parameter is een array met één waarde, color. Het hele arguments-object van ClassB kan als tweede parameter worden doorgegeven aan de apply() methode:

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

Natuurlijk kan een parameterobject alleen worden doorgegeven als de volgorde van de parameters in de superklasse volledig overeenkomt met de volgorde in de subklasse. Als dit niet het geval is, moet er een aparte array worden gemaakt, waarin de parameters in de juiste volgorde worden geplaatst. Bovendien kan de call() methode worden gebruikt.

Prototype-chaining (prototype chaining)

Dit soort erfenis wordt in ECMAScript oorspronkelijk gebruikt voor het prototype-chaining. In het vorige hoofdstuk hebben we het prototype-wijze definiëren van klassen besproken. Het prototype-chaining breidt deze manier uit en implementeert het inheriteermechanisme op een interessante manier.

In het vorige hoofdstuk hebben we geleerd dat het prototype-object een sjabloon is, waarop alle te instantieerden gebaseerd zijn. Kortom, alle eigenschappen en methoden van het prototype-object worden doorgegeven aan alle instanties van die klasse. Het prototype-chaining maakt gebruik van deze functionaliteit om het inheriteermechanisme te realiseren.

Als je de klasse op prototype-wijze opnieuw definieert, zal deze de volgende vorm aannemen:

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

Het magische van de prototype-methode ligt in de benadrukte blauwe regels. Hier wordt de prototype-eigenschap van ClassB ingesteld op een instantie van ClassA. Dit is erg interessant, omdat je alle eigenschappen en methoden van ClassA wilt, maar je wilt ze niet individueel toewijzen aan de prototype-eigenschap van ClassB. Is er een betere manier dan het toewijzen van een instantie van ClassA aan de prototype-eigenschap?

Opmerking:De constructor van ClassA wordt aangeroepen zonder parameters door te leveren. Dit is de standaardpraktijk in de prototype-chain. Zorg ervoor dat de constructor geen parameters heeft.

Op dezelfde manier als met objectvervanging moeten alle eigenschappen en methoden van de subklasse verschijnen na het toewijzen van de prototype-eigenschap, omdat alle methoden die voor die tijd zijn toegewezen, worden verwijderd. Waarom? Omdat de prototype-eigenschap wordt vervangen door een nieuw object, en het oorspronkelijke object dat de nieuwe methoden toevoegde, wordt vernietigd. Daarom is de code om de eigenschap name en de methode sayName() toe te voegen aan de ClassB-klasse als volgt:

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

Je kunt dit gedeelte van de code testen door de volgende voorbeeldcode uit te voeren:

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

TIY

Daarnaast werkt de instanceof-bewerking in de prototype-chain ook uniek. Voor alle instanties van ClassB returnt instanceof zowel ClassA als ClassB true. Bijvoorbeeld:

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

In de zwakke typewereld van ECMAScript is dit een uiterst nuttige tool, maar je kunt het niet gebruiken bij objectvervanging.

Een van de nadelen van de prototype-chain is dat het meervoudige erfenis niet ondersteunt. Onthoud, de prototype-chain overschrijft de prototype-eigenschap van de klasse met een ander type object.

Mengmethode

Deze manier van erfenis gebruikt een constructor om een klasse te definiëren, niet enige prototype. Het belangrijkste probleem van objectvervanging is dat je de constructor-methode moet gebruiken, wat niet de beste keuze is. Maar als je de prototype-chain gebruikt, kun je geen constructor met parameters gebruiken. Hoe kiezen ontwikkelaars? Het antwoord is eenvoudig, beide gebruiken.

In het vorige hoofdstuk hebben we uitgelegd dat het beste manier om een klasse te maken is om eigenschappen te definiëren met constructiefuncties en methoden met prototypes. Deze manier is ook van toepassing op het erfmechanisme, waarbij eigenschappen van de constructor worden geërfd met objectvervanging en methoden van het prototype-object worden geërfd via de prototype-ketting. Met deze twee manieren kan de voorbeeldcode worden herschreven als volgt:

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

In dit voorbeeld wordt het erfmechanisme uitgevoerd door twee doorgezette blauwe regels. In de eerste doorgezette regel, in de constructor van ClassB, wordt de eigenschap sColor van ClassA geërfd met objectvervanging. In de tweede doorgezette regel wordt de methode van ClassA geërfd via de prototype-ketting. Omdat deze mixed mode de prototype-ketting gebruikt, werkt de instanceof-bewerking correct.

Hieronder wordt een voorbeeld getest van dit code:

var objA = new ClassA("blauw");
var objB = new ClassB("rood", "John");
objA.sayColor(); // Uitvoer "blauw"
objB.sayColor(); // Uitvoer "rood"
objB.sayName(); // Uitvoer "John"

TIY