Tekijät: Marko Haanranta, Kasper Kivikataja, Kati Kyllönen, Jussi Miestamo
Pääsivu
2. viikko
3. viikko
4. viikko
Javascriptin oliot ja periytyminen käsitetään eri tavalla kuin perinteisissä olio-ohjelmointikielissä. Javassa on luokat, joissa määritellään olioiden ominaisuudet, ja yksittäisillä olioilla, jotka ovat luokan ilmentymiä, on arvot noille ominaisuuksille. Luokille voidaan luoda aliluokkia, jotka sitten perivät yliluokkansa ominaisuudet. Aliluokassa voidaan joko muuttaa yliluokan ominaisuuksia tai tuoda aliluokkaan uusia ominaisuuksia yliluokkaan nähden. Aliluokan ilmentymillä on omien ominaisuuksiensa lisäksi myös yliluokan ominaisuudet. Myös Javascriptissä oliot voivat periä toisten olioiden ominaisuuksia, mutta järjestelmä on perustavanlaatuisesti erilainen. Luokkia ei ole. Javascriptissä käytetään prototyyppiperintää. Oliolla on omien ominaisuuksiensa lisäksi prototyyppiolionsa ominaisuudet, jolla on omiensa lisäksi prototyyppiolionsa ominaisuudet, jolla on omiensa lisäksi prototyyppiolionsa ominaisuudet. Ketju jatkuu, kunnes päästään perimysketjun huipulle Object-olioon. Javascriptissä oliot siis perivät toisten olioiden ominaisuuksia eivätkä luokkatasolla määrättyjä ominaisuuksia. Jos on määritelty konstruktori oliolle a
ja tuolla konstruktorilla luodaan b
, kaikki ne oliot, jotka perivät ominaisuuksia a
:lta perivät vain a
:n ominaisuudet. Vaikka b
:hen lisättäisiin kenttiä, a
:n "perilliset" eivät perisi b
:n uusia ominaisuuksia.
Mitä ohjelmointityyliin tulee, olio-ohjelmoinnissa pätevät samat säännöt ja tavoitteet kuin olio-ohjelmoinnissa yleensä: reaalimaailman entiteettejä on pyrittävä mallintamaan ohjelmakoodissa, siten että koodi olisi ymmärrettävää, ja ohjelmakoodi on rakennettava niin, että turhaa koodin toistoa ei ole.
Eräs määrittely Javascriptin oliolle on seuraava: "Olio on kokoelma ominaisuuksia, joilla on yksi yhteinen prototyyppiolio. Prototyyppi voi olla joko olio tai null
[2]." Toisaalta olio voidaan määritellä myös seuraavasti: "Javascriptissä kaikki käyttäytyvät oliomaisesti, paitsi undefined
ja null
[1]."
Yleisesti mielletään, että oliot ovat funktioiden ilmentymiä, jotka luodaan käyttämällä avainsanaa new
. Olioita voi ajatella myös avain-, arvopareina, joita käytetään, kuten Hashmap-tietorakennetta.
Emme löytäneet mitään perusteluja sille, miksi Javascriptin oliosuunnittelussa tulisi poiketa oliosuunnittelun yleisistä periaatteista. Näinollen suosittelemme suunnittelemaan oliot esimerkiksi GRASP-periaatteita noudattaen [7]. Javascript tarjoaa kuitenkin erittäin hyvän suunnittelumallin nimeltä Module-pattern, jonka avulla sovelluksen toimintalogiikkaa voidaan kapseloida hyödyntämällä sulkeumia ja anonyymejä funktioita. Tutustutaan ensin kuitenkin yksinkertaisen olion rakenteeseen.
Esimerkki yksinkertaisesta oliosta ja kuva sen prototyypistä
var foo = {
x: 10,
y: 20
};
Kuvan alkuperä [2]
Käyttämällä olioliteraalinotaatiota{}
saadaan luotua tyhjä olio. Vaikka oliolla ei ole omia ominaisuuksia, se perii kuitenkin Object.prototype:n.
var empty = {}
Olion nimi osoittaa pointterin tapaan objektin prototyyppiin. Jos oliolta haetaan ominaisuutta, jota sille ei ole määritelty, niin javascript etsii sitä ensin olion omasta prototyypistä. Etsintää jatketaan prototyyppiketjua seuraten, kunnes lopulta huomataan, että prototyyppiketju on käyty loppuun asti, eikä ominaisuutta ole löytynyt, jolloin palautetaan arvo undefined
. Huomionarvoista on, että jos olion prototyypistään perimän attribuutin arvoa yritetään muuttaa, niin prototyypin attribuutin arvo ei muutu. Sen sijaan, oliolle luodaan uusi samanniminen kenttä.
Samoin toimitaan, jos olion "määrittelemättömän" kentän arvoa yritetään muuttaa.
Tässä toinen esimerkki prototyyppiketjuista:
var a = {
x: 10,
calculate: function (z) {
return this.x + this.y + z
}
};
var b = {
y: 20,
__proto__: a
};
var c = {
y: 30,
__proto__: a
};
// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80
Kuvan alkuperä [2]
Alla oleva esimerkki esittelee this-viittauksen käyttöä:
function make_person_object(firstname, lastname, age) {
this.firstname = firstname;
this.lastname = lastname;
this.age = age;
}
Esimerkkiobjekteja:
var Joe = make_person_object("Joe", "Smith", 23); // huomaa ei new operaatiota
console.log(Joe); // undefined
Joe
ei siirry viittaamaan luotuun olioon, joka häviää bittiavaruuteen. Tilanne voitaisiin korjata liittämällä konstruktorifunktioon return
-lause, joka palauttaisi this
. Kuitenkin nähdäksemme parempi tapa olisi nähdäksemme:
var John = new make_person_object("John", "Smith", 45); // new operaatio
console.log(John); // {firstname: "John", lastname: "Smith", age: 45}
Tässä käytetään new
-kutsua, mikä johtaa odotettuun tulokseen eli siihen, että John
syntyy. Oikeastaan on sama kumpaa tapaa käyttää, mutta jälkimmäinen tuntuu tutummalta. Varsinaisen ymmärrettävyysongelman tässä aiheuttaa funktion nimentä. Jos funktion nimi on make_person_object
, se ohjaa koodaria ajattelemaan, että funktio palauttaisi jotakin. Jos halutaan käyttää tällaista nimeä, funktion pitäisi palauttaa luomansa objekti. Jos taas halutaan käyttää jälkimmäistä tapaa, olisi parempi nimetä funktio Person
iksi, koska on intuitiivisempaa luoda uusi "henkilö" kuin luoda uusi "luo uusi henkilö" -objekti.
Nyt voidaan olion prototyypille lisätä greet() metodi, joka käyttää olion parametrejä this viitteen avulla. Tulostuksessa tulostetaan juuri lisätty full_name ja aiemmin olioon asetettu this.firstname.
make_person_object.prototype.greet = function(){
console.log("Hello! I'm", this.full_name, "Call me", this.firstname);
};
John.full_name // "N/A"
John.full_name = "John Smith";
make_person_object.full_name // Still "N/A"
John.greet(); // "Hello! I'm John Smith Call me John"
Javascript-ohjelmoinnissa olisi isoissa projekteissa järkevintä käyttää jo aiemmin mainittua module pattern -suunnittelumallia. Mallin idea on "emuloida" luokkien ideaa, eli kapseloida olioiden sisältö siten, että olion sisäistä tilaa pääsee muokkaamaan vain "settereiden" avulla. Suunnittelumalli mahdollistaa sekä julkisten että yksityisten metodien ja muuttujien käytön. Isoissa projekteissa tämä pienentää todennäköisyyttä, että ohjelmoijien tahoillaan luomat rakenteet voisivat sekoittua toisten ohjelmoijien rakenteisiin. [8]
Seuraavassa esimerkki suunnittelumallista.
var testModule = (function () {
var counter = 0;
return {
incrementCounter: function () {
return counter++;
},
resetCounter: function () {
console.log( "counter value prior to reset: " + counter );
counter = 0;
}
};
})();
// Increment our counter
testModule.incrementCounter();
Tässä counter
-muuttujaan pääsee käsiksi ainoastaan moduulin palauttamien metodien avulla. Tämä suunnittelumalli on siis erityisesti edullinen julkisia rajapintoja luotaessa ja suurissa projekteissa, mutta toisaalta tällä ohjelmointityylillä häivytetään Javascriptille ominaista dynaamisuutta. Toki on edelleen niin, että jos testModules
luodaan uusia ilmentymiä, niille voi asettaa omia kenttiä mielin määrin.
Olio-ohjelmoinnissa, kuten ohjelmoinnissa yleensäkin tulee vastaan tilanteita, joissa samanlaisia, tai miltei samanlaisia ohjelmanosia tarvitaan useassa kohdassa ohjelmakoodia. Helppo ratkaisu on kopioida tarvitut ohjelmanosat sinne missä niitä tarvitaan. Tämä on luonnollisestikin hyvän ohjelmointitavan vastaista ja onkin pyrittävä tekemään koodista mahdollisimman uudelleenkäytettävää. Perintä on yksi keino, jonka avulla voidaan välttyä kirjoittamasta jo kerran kirjoitettua koodia uudestaan. Javascriptissä käytetään prototyyppeihin perustuvaa perintää.
Javascriptillä voidaan myös toteuttaa moniperintä, jossa olio perii ominaisuuksia useilta muilta olioilta.
Esimerkki prototyyppiperinnästä, käyttäen Object.create funktiota. [4]
// Shape - superclass
function Shape() {
this.x = 0;
this.y = 0;
}
// superclass method
Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
console.info("Shape moved.");
};
// Rectangle - subclass
function Rectangle() {
Shape.call(this); // call super constructor.
}
// subclass extends superclass
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
var rect = new Rectangle();
rect instanceof Rectangle // true.
rect instanceof Shape // true.
rect.move(1, 1); // Outputs, "Shape moved."
Lähteet:
[1] http://bonsaiden.github.io/JavaScript-Garden/
[2] http://dmitrysoshnikov.com/ecmascript/javascript-the-core/
[3] http://www.crockford.com/javascript/inheritance.html
[4] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create
[5] http://stackoverflow.com/questions/5958417/javascript-function-and-object
[6] http://yehudakatz.com/2011/08/12/understanding-prototypes-in-javascript/
[7] http://en.wikipedia.org/wiki/GRASP_(object-oriented_design)
[8] http://addyosmani.com/resources/essentialjsdesignpatterns/book/#modulepatternjavascript