ECMAScript-inheritanssimekanismin toteutus

Perinnän toteuttaminen

ECMAScriptin perinnän toteuttaminen vaatii, että aloitat perityltä perusluokalta. Kaikki kehittäjät määrittämät luokat voivat toimia perusluokkina. Tietoturvan vuoksi paikalliset luokat ja isäntäluokat eivät voi olla perusluokkia, jotta estetään yleisen pääsyn käännetyille selainkoodille, koska tällainen koodi voidaan käyttää pahantahtoisesti.

Valitsemasi perusluokan jälkeen voit luoda sen alatyypin. Käyttääkö perusluokkaa on täysin sinun päätettävissäsi. Joskus saattaa olla, että haluat luoda luokan, jota ei voida käyttää suoraan, mutta joka tarjoaa yleisiä funktioita alatyypeille. Tässä tapauksessa perusluokka katsotaan abstrakteksi luokaksi.

Vaikka ECMAScript ei määrittele abstrakteja luokkia yhtä tiukasti kuin muut kielet, se luo joskus luokkia, joita ei voida käyttää. Tällaisia luokkia kutsutaan yleensä abstrakteiksi luokiksi.

Luodut alatyypit perivät ylityypin kaikki ominaisuudet ja metodit, mukaan lukien rakentaja- ja metodin toteutus. Muista, että kaikki ominaisuudet ja metodit ovat julkisia, joten alatyypit voivat suoraan käyttää näitä menetelmiä. Alatyypit voivat myös lisätä uusia ominaisuuksia ja menetelmiä, joita ylityypissä ei ole, tai korvata ylityypin ominaisuudet ja metodit.

Perintätavat

Kuten muutkin toiminnot, ECMAScript:n perintätapa ei ole ainoa. Tämä johtuu siitä, että JavaScriptin perintämekanismi ei ole selkeästi määritelty, vaan se toteutetaan imitoinnilla. Tämä tarkoittaa, että kaikki perintädetalit eivät ole täysin kääntäjän käsittävissä. Kehittäjänä sinulla on oikeus päättää sopivimmasta perintätavasta.

Tässä esitellään useita konkreettisia perintätapoja.

Objektin esittely

Kun suunniteltiin alkuperäistä ECMAScript:ää, ei ollut tarkoitus suunnitella objektin esittelyä (object masquerading). Se kehittyi vasta, kun kehittäjät alkoivat ymmärtää funktioiden toimintaa, erityisesti how to use this-kirjainta funktioiden ympäristössä.

Periaate on seuraava: rakentaja käyttää this-kirjainta kaikkien ominaisuuksien ja metodiin antamiseen (eli käyttää luokan määrittelyssä olevaa rakentajaa). Koska rakentaja on vain funktio, voidaan ClassA:n rakentaja tehdä ClassB:n metodiksi ja kutsua sitä. ClassB saa ClassA:n rakentajassa määritellyt ominaisuudet ja metodit. Esimerkiksi, määritellään ClassA ja ClassB seuraavasti:

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

Muistatko? Avainsana this viittaa rakentajan nykyiseen luokkaan. Mutta tässä menetelmässä this viittaa siihen omistavaan objektiin. Tämä periaate on luoda ClassA:ta yleisenä funktiona perintämekanismin luomiseksi, ei rakentajana. Seuraavalla tavalla voidaan käyttää rakentajaa ClassB perintämekanismin luomiseksi:

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

Tässä koodissa ClassA:lle annetaan metodi newMethod (muista, että funktion nimi on vain osoittaja siihen). Tämän jälkeen kutsutaan tätä metodia ja annetaan sille ClassB:n rakentajan parametri sColor. Viimeinen rivi poistaa viittauksen ClassA:hen, joten sitä ei voi enää kutsua.

Kaikki uudet ominaisuudet ja metodit on määriteltävä vasta sen jälkeen, kun on poistettu uuden metodin koodirivi. Muuten voi tapahtua yliluokan vastaavien ominaisuuksien ja metodiin liittyvien tietojen korvaaminen:

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

Edellä olevan koodin tehokkuuden osoittamiseksi voidaan suorittaa seuraava esimerkki:

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

Objektin esittely voi toteuttaa monikielisyyden

On mielenkiintoista, että objektin esittely voi tukea monikielisyyttä. Tämä tarkoittaa, että luokka voi perivänsä useita yliluokkia. Monikielisyyden mekanismi UML:llä esitetään seuraavassa kuvassa:

Inheritanssimekanismi UML-diagrammin esimerkki

Esimerkiksi, jos on olemassa kaksi luokkaa ClassX ja ClassY, ClassZ haluaa perivänsä nämä kaksi luokkaa, voidaan käyttää seuraavaa koodia:

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

Tämä esimerkki on sama kuin edellinen esimerkki, mutta nyt kutsutaan apply() -menetelmää. Kun kutsutaan apply() -menetelmää, ensimmäinen parametri on edelleen obj, mikä tarkoittaa, että sayColor() -funktioon annettava this-kohde on obj. Toinen parametri on kaksi merkkijonoa muodossa, joka vastaa sayColor() -funktioon sijoitettuja sPrefix ja sSuffix -parametreja, ja lopputuloksena oleva viesti on edelleen "The color is blue, a very nice color indeed." ja se näytetään.

Tämä menetelmä käytetään myös korvaamaan edelliset kolme riviä asettamisesta, kutsusta ja uuden metodin poistamisesta:

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

Samoin, ensimmäinen parametri on edelleen this, ja toinen parametri on vain yksi arvon omaava taulukko color. voidaan siirtää koko ClassB:n arguments-objekti apply() -menetelmälle:

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

Totta kai, parametrien siirto parametripäätteisiin tapahtuu vain, jos yliluokan ja aliluokan parametrien järjestys on täysin sama. Jos ei ole, on luotava erillinen taulukko, jossa parametrit sijoitetaan oikeaan järjestykseen. Lisäksi voidaan käyttää call() -metodia.

Prototyypin ketju (prototype chaining)

Tämä muoto perintää ECMAScriptissä on alun perin suunniteltu prototyypin ketjulle. Edellisessä luvussa esiteltiin luokan määrittäminen prototyypin avulla. Prototyypin ketju laajentaa tätä tapaa ja toteuttaa perintämekanismin mielenkiintoisella tavalla.

Edellisessä luvussa opetettiin, että prototype-objekti on malli, ja kaikki instanssit perustuvat tähän malliin. Yhteenvetona, prototype-objektin kaikki ominaisuudet ja metodit siirretään kyseisen luokan kaikkiin instansseihin. Perinnäisjärjestelmä hyödyntää tätä toimintoa perintämekanismin toteuttamiseksi.

Jos määritellään luokka prototyypin avulla edellisessä esimerkissä esiteltyjä luokkia, ne muuttuvat seuraavaksi muodoksi:

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

Prototyypin tapa loistaa korostetulla sinisellä koodirivillä. Tässä ClassB:n prototype-ominaisuus asetetaan ClassA:n instanssiksi. Tämä on mielenkiintoista, koska halutaan ClassA:n kaikki ominaisuudet ja metodit, mutta ei haluta lisätä niitä yksitellen ClassB:n prototype-ominaisuuteen. Onko parempaa tapaa asettaa ClassA:n instanssi prototype-ominaisuudeksi?

Huomaa:Kutsutaan ClassA:n rakentajafunktiota, ei anneta sille mitään parametreja. Tämä on prototyypin ketjun standardikäytäntö. Varmista, että rakentajafunktio ei sisällä mitään parametreja.

Kuten objektin teeskentelyyn, kaikki lapsiluokan ominaisuudet ja metodit täytyy olla mukana prototype-ominaisuuden asettamisen jälkeen, koska ennen sen asettamista kaikki metodit poistetaan. Miksi? Koska prototype-ominaisuus korvataan uudella objektilla, ja alkuperäinen objekti, joka lisäsi metodeja, tuhoutuu. Siksi ClassB-luokalle lisätään name-ominaisuus ja sayName()-metodi seuraavasti:

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

Tämä koodi voidaan testata suorittamalla seuraava esimerkki:

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

TIY

Lisäksi prototyypin ketjussa instanceof-laskutoimituksen toiminta on myös erityinen. Kaikille ClassB:n instansseille instanceof palauttaa true sekä ClassA että ClassB. Esimerkiksi:

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

ECMAScriptin heikkotyypillisessä maailmassa tämä on erittäin hyödyllinen työkalu, mutta sitä ei voi käyttää objektin teeskentelyssä.

Prototyypin ketjun haitta on, että se ei tue monikertaisia perintöjä. Muista, että prototyypin ketju korvaa luokan prototype-ominaisuuden toisen tyyppisellä objektilla.

Yhdistelmätapa

Tämä perintätapa käyttää rakentajafunktiota luokan määrittämiseen, ei mitään prototyyppejä. Kohteen teeskentelyn pääongelma on, että on käytettävä rakentajafunktiota, mikä ei ole paras mahdollinen valinta. Mutta jos käytetään prototyypin ketjua, ei voi käyttää parametria sisältävää rakentajafunktiota. Miten kehittäjä valitsee? Vastaus on yksinkertainen, molemmat yhdessä.

Edellisessä luvussa olemme käsitelleet parasta tapaa luoda luokkia: käyttää konstruktoreita ominaisuuksien määrittämiseen ja prototyyppejä metodien määrittämiseen. Tämä tapa soveltuu myös perintämekanismiin, jossa konstruktoreiden ominaisuudet peritään objektin avulla ja prototype-objektin metodit peritään prototyypin ketjun avulla. Tällä tavalla voidaan uudelleen kirjoittaa edellinen esimerkki seuraavasti:

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

Tässä esimerkissä perintämekanismi toteutetaan kahdella korostetulla sinisellä rivillä. Ensimmäisessä korostetussa rivissä ClassB-konstruktorissa, sColor-ominaisuus peritään ClassA-luokan kautta objektin avulla. Toisessa korostetussa rivissä ClassA-luokan metodit peritään prototyypin avulla. Koska tämä sekayhdistelmä käyttää prototyypin ketjua, instanceof-laskin toimii edelleen oikein.

Seuraavassa esimerkissä testataan tätä koodia:

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

TIY