Javascript

Tekijät: Marko Haanranta, Kasper Kivikataja, Kati Kyllönen, Jussi Miestamo

1. Johdanto

Hyvän ohjelmointityylin keskeisenä tavoitteena voidaan varmaankin pitää jonkinlaista tasapainoa koodin tehokkuuden ja luettavuuden välillä. Tehokkuus on itsestään selvä ja välttämätön tavoite, ja siihen pitää pyrkiä, muttei kuitenkaan ymmärrettävyyden kustannuksella. Koodaajien on myös pystyttävä seuraamaan ohjelman logiikkaa helposti, jotta säilyisi ymmärrys siitä, mitä ohjelma tekee. Jokaisella ohjelmoijalla on oma tyylinsä, eikä yhtä ehdottoman oikeaa ole mahdollista saati järkevää määrätä. Hyvään ohjelmointityyliin kuuluu kuitenkin joitain valitusta ohjelmointikielestä riippumattomia nyrkkisääntöjä. Niitä ovat esimerkiksi pyrkimys pitää funktiot (tai metodit) lyhyinä sekä yhtenäinen käytäntö luokkien, objektien, primitiivityyppien nimennässä. Tehokkuuteen ja koodin luettavuuteen pitää siis pyrkiä käytettävästä ohjelmointikielestä riippumatta, mutta ohjelmointikielet ovat erilaisia, ja niissä on omat vahvuutensa ja heikkoutensa. Tässä tekstissä keskitytään esittelemään Javascriptin ominaisuuksia.

1.1 Javascriptistä yleensä

Javascript on ohjelmointikieli, joka tunnetaan yleisesti käytöstä web-selainohjelmoinnissa. Käyttö ei rajoitu kuitenkaan tähän, vaan sitä käytetään myös palvelinohjelmoinnissa esim. Node.js-ohjelmistoalustan skriptikielenä. Javascript on oliokieli, jossa ei kuitenkaan (ainakaan vielä) ole luokkia kuten vaikkapa Javassa tai C++:ssa. Siinä käytetään prototyyppiperintää, jossa oliot perivät ominaisuuksia prototyypeiltään, jotka puolestaan perivät ominaisuuksia omilta prototyypeiltään jne. Javascript on hyvin dynaaminen kieli: Olioille voi esimerkiksi suoritusaikana lisätä kenttiä, tai niitä voi poistaa. Lisäksi kieli on dynaamisesti tyypitetty. Muuttujilla ei ole tyyppiä, vain niiden arvoilla. Jos muuttujan arvona ensin ollut vaikkapa Number, ei ole mitään estettä muuttaa saman muuttujan arvoksi seuraavalla koodirivillä String-tyypin oliota tai mitä tahansa muuta, vaikkapa funktiota. Se, mitä tämä tekee ohjelman toiminnan kannalta, jätetään ohjelmoijan vastuulle. Tässä juuri piilee Javascriptin hienous ja vaikeus: ohjelmoija voi tehdä asioita miten huvittaa, mutta toisaalta ohjelmoijalle jää myös paljon vastuuta. Tämän tekstin tarkoitus on esitellä hyviä ja tehokkaita ohjelmointiratkaisuja Javascriptillä ja toisaalta muistuttaa sudenkuopista, joita kielen suoma vapaus tuo mukanaan.

2. Tyyppiturvallisuuden tavoittelua

Javascriptissä muuttujat ja parametrit ovat tyypittömiä, mutta niiden arvoilla on tyypit. Javascriptin tietotyypit ovat merkkijono (string), luku (number), totuusarvo (boolean), taulukko (array), objekti (object), olematon (null) ja määrittelemätön (undefined). Vaikka muuttujilla ei olekaan tyyppiä, on arvojen tyyppejä syytä tarkistaa, jotta ohjelmat eivät tuottaisi odottamattomia tuloksia. Erityisesti käyttäjän syötteitä on hyvä tarkastella virheiden varalta. Javascriptissä yhteenlasku voidaan suorittaa ongelmitta luvun ja String-tyypin olion välillä.

function sum(a,b){
    return (a+b)
}

write(sum(2,"w4"))                          //tulostaa "2w4"

Javascript tarjoaa - kuten tunnettua - funktiot parseInt() ja parseFloat() kokonais- tai liukulukujen eristämiseen syötteestä: jos syöte alkaa numerolla, siitä erotetaan numero-osa, ja loppu jätetään huomiotta. Joissain tapauksessa tämä on täysin riittävä tapa varmistaa se, että syöte on kelvollista. Jos ohjelmoija odottaa saavansa syötteenä esimerkiksi kokonaisluvun, vaaraa ei ole, koska parseInt() palauttaa aina kokonaisluvun tai tietotyypin undefined. Tietysti ohjelmakoodin pitää olla rakennettu niin, että undefined:iin on varauduttu, mutta käytettiin mitä tahansa keinoa tyyppien tarkastamiseen, undefined:iin on joka tapauksessa aina varauduttava, jotta ohjelman toiminta olisi ennakoitavaa.

Vaikka yllä oleva esimerkki on yksinkertainen, pelkkä undefined:iin varautuminen ei riitä. Koska oletettavaa on, että käyttäjä (tai funktiota kutsuva ohjelmoija) ei kirjoita syötteeseen lainausmerkkejä, esimerkin syötteen tekee kelvottomaksi todennäköisemmin käyttäjän lyöntivirhe:

write(sum(2,w4))                   //Tulostaa konsoliin ReferenceError: w4 is not defined;

Tällaisten tilanteiden varalta pitäisikin tarkistaa, onko syöte ylipäätään kokonaisluku. On onnekasta, jos syöte alkaa numeroilla, mutta jos ei ala, parseInt() ei pelasta.

Tyyppien tarkistus ohjelmakoodissa on erityisen tärkeää juuri käyttäjän syötettä käsiteltäessä, sekä muutoinkin ohjelman ulkoisissa rajapinnoissa.

2.1 Funktioita tyyppitarkastusten avuksi

Javascriptillä voisi halutessaan ohjelmoida sarjan apufunktioita tyyppitarkistusten tekemiseksi. Seuraavassa esittelemme mahdollisia tällaisten funktioiden toteutuksia.

function isNumber(value) {
    return typeof value === 'number'
}
function isString(value) {
    return typeof value === 'string'
}

function isObject(value) {
   return typeof value === 'object';
}

function isBoolean(value) {
   return typeof value === 'boolean';
}
function isUndefined (value) {
    return typeof value === 'undefined'
}
function isArray(value) {
    return value instanceof Array;
}
function hasValue(value) {
    if(value){
        return true
    }
    return false
}
function isInteger(value) {
    return value === Math.floor(value)
}
function isDouble (value) { 
    return typeof value === "number" && !isNaN(value) && value !== Math.floor(n)
}
function isFunction(func) {
   var getType = {};
   return func && getType.toString.call(func) === '[object Function]';
}

Seuraavassa esittelemme pienen esimerkkiohjelman, jossa tyyppitarkastuksilla turvataan ohjelman odotettu toiminta. if-lauseen ehto-osassa tarkastetaan käyttäjän syöte: ensin tarkistetaan, että syöte ei ole tyhjä hasValue(), ja jos syöte on annettu tarkistetaan, että se on numeerista isNumber(). Jos nämä ehdot eivät täyty, tulostetaan käyttäjälle virheilmoitus.

var korkeus = parseInt(prompt("Anna kolmion korkeus:"));

if (hasValue(korkeus) && isNumber(korkeus) 
    && korkeus > 0 && korkeus < 22){

    var alimman_leveys = 1 + ((korkeus-1)*2);
    var taman_leveys = 1;
    var tyhjia = (alimman_leveys-taman_leveys)/2;

  for (var i = 0 ; i < korkeus; i++){
    var tyhjat = new Array(tyhjia+1).join(" ");
    var tahdet = new Array(taman_leveys+1).join("*");

    write(tyhjat + tahdet);
    taman_leveys = taman_leveys +2;
    tyhjia = (alimman_leveys-taman_leveys)/2;
  }

}
else{
  alert("Ei käy");
}

2.2 Yleisen tason pohdiskelua tyyppiturvallisuuden tavoittelusta

Voidaan tietysti pohtia, kuuluuko dynaamisesti tyypitettyjen kielten luonteeseen lainkaan tyyppiturvallisuuden tavoittelu. Joissain tilanteissa se voi olla tarpeen, mutta tarjolla on muitakin keinoja tarkistaa käyttäjältä tuleva syöte. Voidaan käyttää esimerkiksi Javascript-ohjelmointikehysten lomakevalidaattoreita tai säännöllisiä lausekkeita. Kun puhutaan kokonaislukutyypin ja liukulukutyypin tarkistuksista Javascriptillä, on oikeastaan kysymys hiukan eri asiasta kuin Javassa tai C:ssä. JS:ssä noiden tyyppien erottelua ei nimittäin ole rakennettu kieleen sisään, vaan molemmat ovat tyyppiä number. Varmaa on joka tapauksessa se, että tyyppitarkistusten tekeminen ei ole pakollista toisin kuin tiukasti tyypitetyissä kielissä, mutta kuitenkin usein se voi olla tarpeen ohjelmien ennustettavan toiminnan takaamiseksi.

3. Algoritmit, funktiot ja sulkeumat, poikkeusten käsittely

3.1 Algoritmit ja JavaScript

Javascriptille on luonteenomaista jättää paljon ohjelmoijan vastuulle. Siihen, mikä on hyvää ohjelmointityyliä, tämä vaikuttaa eri tilanteissa eri tavoilla. Jos käyttäjältä pyydetään syötteenä jotakin numeerista, ei ohjelma välttämättä kaadu, jos syötteen seassa on kirjaimia. Kuitenkin pitää ottaa huomioon, mihin syötettä on tarkoitus käyttää. Javascript-ohjelmoijan sen sijaan pitää itse pitää huoli siitä, että käyttäjän syöte ei etene vaikkapa seuraavan funktion kutsuun parametrina ja aiheuta jotakin odottamatonta, vaikkapa ohjelman kaatumista. Javascriptiä ajettaessa virheellinen syöte saattaa edetä hyvinkin pitkälle suoritusaikana ennen kuin virhe ilmenee, ja tällöin sen löytäminen on vaikeampaa. Web-sovelluksen käyttäjälle ohjelman kaatuminen voi aiheuttaa sen, että ruudulla näkyy toivotun sisällön sijasta pelkkä valkoinen selainikkuna. Käytti sitten mitä tahansa ohjelmointikieltä, syötteeseen on varauduttava jotenkin. JS:n tapauksessa "virheellinen" syöte ei siis kaada ohjelmaa ainakaan heti, mutta ohjelmoijan on pidettävä huolta, ettei se riko myöhemmin esimerkiksi tietokannan toimintaa.

Mietitään hetki tilannetta, jossa käyttäjän syöte luetaan value-muuttujaan, ja muuttujan arvo tarkistetaan aiemmin esillä olleella isInteger-funktiolla. Varsinaista etua JS:n tyypittömyydestä ei tässä ole. JS:llä tässä tilanteessa ei välttämättä edes tarvittaisi tyyppitarkistusta, vaan riittäisi, että testattaisiin vaikkapa säännöllisellä lausekkeella, koostuuko kentästä saatu merkkijono vain numeroista. Tämä voitaisiin toteuttaa myös try-catch-rakenteella, mutta kaikkien tarkistusten tekeminen poikkeuksia heittelemällä ei tee hyvää ohjelman tehokkuudelle.

Tyypittömyyden hyödyt ja haitat tulevat esille oikeastaan tilanteittain. Dynaaminen tyypitys voi vapauttaa ohjelmoijan tekemään asioita kompaktisti ja tehokkaasti, mutta toisaalta se siirtää vastuun ohjelmoijalle, ja huolimattomuudesta voi seurata kauheita. Seuraava esimerkki on vielä helpohkosti hahmotettavissa, mutta entä jos idea vietäisiin pidemmälle? Missä tulee vastaan raja, jonka jälkeen ohjelmoija ei itsekään ymmärrä omaa koodiaan.

summa2 = function(a){return a+a}
potenssi2 =  function(a){return a*a}
a = [summa2,potenssi2]                         // Luo funktioista koostuvan taulukon
for (i=0;i<a.length;i++)
{
  console.log(a[i](3))                         // Tulostaa 6 ja 9
}

Javascriptistä sanotaan, että se on helppo kieli, mutta dynaamisen tyypityksen tehokas hyödyntäminen vaatii ymmärrystä ja huolellisuutta.

Se, että Javascript antaa muuttaa tietorakenteiteita dynaamisesti tekee algoritmien koodaamisesta joustavaa. Esimerkiksi taulukon täyttyessä ei tarvitse kopioida alkioita isompaan taulukkoon. Myös puuttuvan kentän lisääminen tietorakenteeseen käy helposti, mikä tekee koodista helposti muunneltavaa. On oikeastaan pelottavaa ajatella, kuinka voimakas kieli Javascript on ja toisaalta kuinka moni täysin ohjelmointitaidoton sitä käyttää.

3.2 JavaScript ohjelmointi: Funktionaalista vai imperatiivista?

 Seuraavaksi katsomme esimerkin avulla funktionaalisen ja imperatiivisen ohjelmointityylin eroja. Esimerkkifunktiot hakevat taulukosta kaikki objektit joiden arvo on < 10.

Esimerkki 1. Imperatiivinen ohjelmointityyli:

function bestPrices(inputArray) {
   var outputArray = [];
for(var i = 0; i < inputArray.length; i++) {
   if(inputArray[i].price < 10){
      outputArray.push(inputArray[i]);
   }
}
return outputArray;
}
Esimerkki 2. Funktionaalinen ohjelmointityyli:
function bestPrices(inputArray){
   return Array.filter(inputArray, function(value){
   return value.price < 10;
   }
}
Funktionaalisen ohjelmoinnin tekniikoista otamme ensin esimerkiksi anonyymit eli nimettömät funktiot. Seuraavassa esimerkissä olevalle funktiolle ei suoranaisesti anneta nimeä, vaan se annetaan suoraan arvoksi muuttujalle. Tämä esimerkki ei ole varsinaisesti kovin hyödyllinen.
var flyToTheMoon = function()
{
  alert("Zoom! Zoom! Zoom!");
}
flyToTheMoon();

Yleisesti anonyymejä funktioita käytetään hyödyksi tilanteissa, joissa funktiolle täytyy antaa parametrina toinen funktio ja toisaalta sulkeumien yhteydessä. Sulkeumat ovat hyvin vahva ohjelmointitekniikka.

On mahdotonta sanoa, onko toinen ohjelmointityyli toista parempi. Funktionaalinen koodi on usein lyhyempää, tosin raskaampaa ja vaatii aivan toisenlaista ajattelutapaa. Imperatiivisesti kirjoitettu koodi on usein pidempää. Kukin oppii ohjelmoimaan eri tyyleillä, eikä ole mitään yhtä oikeaa ohjelmointityyliä. On hyvä tiedostaa projektin tarpeet ja pitää huoli siitä, että koodi pysyy helposti ylläpidettävänä. Ylläpidettävyyden kannalta olisimme kuitenkin yhdenmukaisen tyylin kannalla, oli valinta sitten funktionaalinen tai imperatiivinen.

3.3 Sulkeumat

Sulkeuma luodaan kirjoittamalla funktio toisen funktion määrittelyn sisään. Tällöin muodostuu näkyvyysalue, jossa sisemmällä funktiolla on pääsy ulomman funktion muuttujiin. Eli ulompi funktio sulkee sisäänsä omat muuttujansa ja sisällään määritellyt funktiot. Sulkeumat tallentavat viittauksen ulomman funktion muuttujiin, eivät itse muuttujan arvoa. Kun ulomman funktion suoritus päättyy, niin sisemmän funktion elämä ei pääty. Sisempää funktioita voidaan kutsua ja sillä on yhä pääsy ulomman funktion muuttujiin.

Sulkeumat ovat osa module pattern suunnittelumallia. Alunperin Module pattern määriteltiin tapana mahdollistaa yksityinen ja julkinen kapselointi luokille tavanomaisessa ohjelmistokehityksessä. Javascriptissä module patternia käytetään jäljittelemään luokkien konseptia siten, että voimme sisällyttää sekä julkisia että yksityisiä metodeja ja muuttujia yhden olion sisälle eristämällä ne globaalista näkyvyysalueesta. 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. Suunnittelumalli on siis erityisesti edullinen julkisia rajapintoja luotaessa ja suurissa projekteissa [9].

Alla olevassa esimerkissä muodostetaan kaksi sulkeumaa increment ja get joita käyttäen päästään käsiksi count muuttujaan sijoitettuun arvoon vaikka Counter funktion suoritus on jo päättynyt. Olennaista on myös huomioida, että konstruktori funktion kutsumisen jälkeen count muuttujaan ei päästä käsiksi mitään muuta kautta kuin näitä kahta metodia käyttäen.

function Counter(start) {
    var count = start;
    return {
        increment: function() { 
            count++;
        },

        get: function() {
            return count;
        }
    }
}

var foo = Counter(4);
foo.increment();
write(foo.get()); // 5

3.4 Poikkeukset

JavaScriptin virhetilanteet voidaan jaotella loogisiin virheisiin, syntaksivirheisiin ja ajonaikaisiin virheisiin. Loogisilla virheillä tarkoitetaan tilanteita, joissa ohjelman looginen toiminta on suunniteltu väärin. Syntaksivirheillä tarkoitetaan JavaScriptin kielen syntaksin vastaisia muotoja, jäsennysvirheitä, jotka JavaScript tulkki havaitsee. Ajonaikaiset virheet tapahtuvat ohjelman suorituksen aikana ja niitä kutsutaan myös nimellä poikkeukset. Tarkastelemme seuraavaksi ajonaikaisia virheitä tarkemmin.

Ajonaikaiset virheet voidaan käsitellä JavaScriptin poikkeuskäsittelyn, eli try -lauseen avulla. Try lause koostuu try, catch ja finally lohkoista. Ajonaikaisia virheitä, jotka laukaisevat poikkeuskäsittelyn on EcmaScriptin määrittelyssä [4] mainittu kuusi:

function poikkeusDemo(){
    try{
         olematonfunktio()
         alert('On olemassa...')
    } catch(err){
         alert('Funktiokutsu meni pieleen: '+err.message)
    } finally{
         alert('Tämä suoritetaan aina!')
    }
}

Kun try lohkossa olevaa koodia suoritetaan ja tapahtuu ajonaikainen virhe, niin suoritus siirtyy välittömästi catch lohkoon ja err muuttuja saa arvokseen Error objektin. Error objektin avulla voi hakea virheestä generoidun virheviestin, tai esimerkiksi virheen nimen. Finally lohkossa oleva koodi suoritetaan aina, tapahtui poikkeuksen heittävä virhe tai ei.

Useita try lausekkeita voi sijoittaa sisäkkäin. Tämä on tarpeen, jos catch lohkossa tehdään toimenpiteitä, jotka voivat johtaa uuteen poikkeustilanteeseen. Jos sisemmässä try lauseessa ei ole catch lohkoa, niin suoritetaan ulomman try lausekkeen catch lohko, jos sellainen on olemassa. Jotkin selaimet, kuten Chrome ja Firefox tosin kieltäytyvät toimimasta ilman sisempää catch tai finally lohkoa, vaan generoivat SyntaxError virheen tästä tilanteesta.

function poikkeusDemoSisempiTry(){
    try{
         olematonfunktio()
         alert('On olemassa...')
    } catch(err){
        alert('Funktiokutsu meni pieleen: '+err.message) 
        try{ 
             write(virheviesti)
        }catch(err){
         alert('Yritetään jatkaa: '+err.message)
        }   
    } finally{
         alert('Tämä suoritetaan aina!')
    }
}

Poikkeuksen voi heittää throw lauseen avulla. JavaScriptissä voi käyttää joko valmiiksi määriteltyjä poikkeuksia tai generoida virhetilanteeseen sopivan poikkeuksen itse. Itsemääritellyissä poikkeuksissa on tärkeää tarjota mahdollisimman tarkka kuvaus siitä mitä on tapahtunut, jotta tieto ongelmatilanteesta saadaan välitettyä eteenpäin mahdollisimman yksityiskohtaisesti tarvittavia toimenpiteitä varten. Lisäksi on suotavaa määritellä poikkeukset hierarkisesti, jotta ongelman käsittelyn tarkkuutta voidaan säätää ohjelmakoodista käsin kutakin tarvetta vastaavaksi ilman, että usean virhetilanteen kaappaamiseksi tarvitsee listata useita poikkeuksia.

Poikkeuksia pitäisi mielestämme käyttää vain erityisen poikkeuksellisissa tilanteissa. Tälläisiä tilanteita voisivat olla esimerkiksi että ohjelman käyttämä tiedosto on varattu tai korruptoitunut. Tai tilanne jossa ohjelma tarvitsee suorittaakseen jotakin tiettyä resurssia, mutta ei ole onnistunut tätä varaamaan toistuvista yrityksistä huolimatta. Jonkinlaisena ohjenuorana kehoittaisimme pohtimaan voiko ohjelma toipua tilanteesta itse, jos voi, niin silloin ei poikkeusta tarvitse heittää.

Poikkeusten käyttäminen normaaleissa virhetilanteissa, kuten käyttäjän syötteen tarkastamisessa ei mielestämme ole suositeltavaa. Poikkeusten heittäminen ja käsittely on suhteellisen raskas toimenpide, sillä poikkeus aiheuttaa muuttujan luomisen ja tuhoamisen ajonaikaisesti. Kaikki selaimet eivät myöskään toimi poikkeustenkaan suhteen samalla tavalla ja myös virheilmoitukset voivat olla erilaisia. Ohjelman suorituskyvyn kannalta parempi vaihtoehto on käyttäjän syöttäjän tarkastaminen ehtolauseella ja tarvittavan statustiedon näyttäminen käyttäjälle mahdollisia korjaavia toimenpiteitä varten.

4. Hyviä ohjelmointikäytäntöjä: oliot ja periytyminen

Javascriptin oliot ja periytyminen käsitetään eri tavalla kuin perinteisissä olio-ohjelmointikielissä. Javascriptissä oliot voivat periä toisten olioiden ominaisuuksia, mutta 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.

4.1 Javascript ja Oliot

Eräs määrittely Javascriptin oliolle on seuraava: "Olio on kokoelma ominaisuuksia, joilla on yksi yhteinen prototyyppiolio. Prototyyppi voi olla joko olio tai null [10]." Toisaalta olio voidaan määritellä myös seuraavasti: "Javascriptissä kaikki käyttäytyvät oliomaisesti, paitsi undefined ja null [9]."

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äin ollen suosittelemme suunnittelemaan oliot esimerkiksi GRASP-periaatteita noudattaen [15]. 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
};

Kuva olion prototyypistä

Kuvan alkuperä [10]

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

Kuva prototyyppiketjusta

Kuvan alkuperä [10]

This-viittauksen käyttö:

function make_person_object(firstname, lastname, age) {
    this.firstname = firstname;
    this.lastname = lastname;
    this.age = age;
}

Tässä uusi olio luodaan käyttämällä new-kutsua.

var Joe = new make_person_object("Joe", "Smith", 23);
console.log(Joe); // {firstname: "Joe", lastname: "Smith", age: 23}

Nyt voidaan olion prototyypille lisätä -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); 
};
Joe.full_name // "N/A"
Joe.full_name = "Joe Smith"; 
make_person_object.full_name // yhä "N/A"
Joe.greet(); // "Hello! I'm Joe Smith Call me Joe"

Oliolle voi määritellä kenttiä eli attribuutteja ja niiden ominaisuuksia objektin defineProperties-funktion avulla. Tälläisiä määriteltäviä ominaisuuksia ovat value, writable, enumerable ja configurable. Value on attribuutin arvo. Writable tarkoittaa, sitä saako attribuutin arvoa muuttaa. Enumerable tarkoittaa sitä, onko attribuutti numeroituva, eli kun olion kenttiä iteroidaan läpi, niin onko kyseinen attribuutti iteroitavien joukossa. Configurable:n avulla määritellään, saako attribuuttia muuttaa ja saako sen poistaa objektista.

var book = {}; // Luo uuden olion

// Lisätään kirja-oliolle data attribuutti ja määritellään sille ominaisuudet
Object.defineProperty(book, "isbn10", {value :  0596517742,
                               writable : false,
                               enumerable : true,
                               configurable : false});

console.log(book.isbn10)

// tulostuu
596517742


// yritetään muuttaa attribuutin isbn10 arvoa, mutta vaikka tämä näyttää onnistuvan, 
// niin attribuutin arvo ei muutu, sillä attribuutin ominaisuus writable on asetettu arvoon false

book.isbn10 = 1

console.log(book.isbn10)

// tulostuu yhä
596517742

// Tässä numeron pitäisi oikeastaan käyttäytyä stringin tavoin, koska isbn10-numerossa
// ensimmäinen numero on merkitsevä, mutta saanemme tämän anteeksi.

4.2 Perintä Javascriptissä

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. Jos perintää ei erikseen määritellä niin luotu olio perii Object.prototypen.

Javascriptissä käytetään prototyyppeihin perustuvaa perintää. Jokaisella oliolla on yksi prototyyppi. Prototyypit muodostavat ketjun joka loppuu Object-olion prototyyppikenttään jossa on arvo null. On myös mahdollista katkaista prototyyppiketju asettamalla perittävän olion tilalle arvo null. Olion prototyyppiolio eli se olio jolta omaisuuksia peritään voidaan määritellä usealla eri tavalla. Tämä johtuu osittain siitä että myös olio voidaan määritellä monin eri tavoin.

Yksi tapa tehdä perinnän määrittely on käyttäen Object.create-funktiota: Rectangle.prototype = Object.create(Shape.prototype). Alla yksinkertainen esimerkki siitä, miten perintä voidaan toteuttaa Object.createa käyttämällä.

var elain = { syo: "tosi" }

var pupu = Object.create(elain) // perintä
pupu.hyppii="tosi"

write(pupu.syo)             //tosi
write(pupu.hyppii)          //tosi
Luodaan ensin olio elain jolla on attribuutti syo. Seuraavaksi luodaan olio pupu, joka perii elain-olion. Seuraavaksi lisäämme pupulle kentän hyppii. Lopuksi tulostamme kenttien sisällöt ja voimme todeta niiden perusteella, että pupu on perinyt kentän syo.

Perintä voidaan tehdä myös käyttäen tuttua avainsanaa new: Rectangle.prototype = new Shape(). Lisäksi on olemassa myös epästandardi - tosin käyttökelpoinen - tapa toteuttaa perintä sijoittamalla arvoja suoraan __proto__-kenttiin: Rectangle.prototype.__proto__=Shape.prototype.

Toinen esimerkki prototyyppiperinnästä, käyttäen new avainsanaa. [12]

// 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 = new Shape();
Rectangle.prototype.constructor = Rectangle;

var rect = new Rectangle();

rect instanceof Rectangle // true.
rect instanceof Shape // true.

rect.move(1, 1); // Outputs, "Shape moved."

Ylläolevassa esimerkissä luotiin ensin Shape-olion konstruktorifunktio jolle asetettiin kentät x ja y. Tämän jälkeen Shape-oliolle lisättiin kenttä move. Seuraavaksi määriteltiin Rectangle-olion konstruktorifunktion. Varsinainen perintä tapahtuu kun Rectangle-olion prototyypiksi laitetaan Shape.prototype. Perinnän jälkeen on vielä hyvä tapa laittaa perijän konstruktorifunktio osoittamaan takaisin perijään: Rectangle.prototype.constructor = Rectangle. Nyt Rectangle konstruktorifunktiota kutsumalla luodut oliot perivät Shape-olion kentät.

Javascriptillä voidaan myös toteuttaa moniperintä, jossa olio perii ominaisuuksia useilta muilta olioilta. Tällöin saadaan olion käyttöön useiden muiden olioiden omaisuudet, mutta menetetään yhteys olion prototyyppiketjuun eli perittävään olioon tehtävät muutokset eivät välity perijälle. Jos moniperintää haluaa käyttää niin löytyy ainakin Ring.js-Javascript-kirjasto sen toteuttamista helpottamaan. Moniperintä ei ole ECMAScript-standardin mukainen ominaisuus, joten ei siitä sen enempää.

5. Lopuksi

Javascript ei ole mikään kuollut kieli vaan sitä kehitetään koko ajan. Nykyisin käytössä on ECMAScriptin versio 5. Jotkut selaimet kuten Mozilla Firefox tukevat jo myös osaa version 6.0 omaisuuksista. Javascriptin versio 6.0 tukee monesta muusta ohjelmointikielestä tuttuja tietorakenteita Map ja Set. Tosin on muistettava että selainten tekijät päättävät itse, mitä ECMAScript-standardin määrityksiä he toteuttavat. Tämän vuoksi kaikkien määriteltyjen ominaisuuksien saaminen käyttöön ei ole mikään itsestäänselvyys. Toisaalta, joku selainvalmistaja voi myös tuoda uusia ominaisuuksia selaimeensa ennenkuin ne on lisätty virallisesti mihinkään Javascriptin määritelmään.

Javascript-ohjelmoijan ei suinkaan tarvitse kirjoittaa kaikkea koodia itse, vaan hän voi käyttää hyväkseen valmiita Javascript-kirjastoja moniin eri tarpeisiin. Tälläisiä kirjastoja ovat esimerkiksi jQuery, Underscore, Midori ja AngularJS. Jos haluaa välttää Javascriptin kirjoittamisen, voi käyttää käännösohjelmia, joilla muulla kielellä kirjoitettu koodi käännetään Javascriptiksi.

Lähteet:

[1] JavaScript Data Types / 2013 http://www.w3schools.com/js/js_datatypes.asp

[2] Ohjelmointitekniikka (JavaScript) / Arto Wikla 2013 http://www.cs.helsinki.fi/u/wikla/OTjs/materiaalia/tyyppeja/

[3] Handling the unexpected – Type safe functions in Javascript / Matthias Reuter 2009 http://united-coders.com/matthias-reuter/handling-the-unexpected-type-safe-functions-in-javascript/

[4] Standard ECMA-262 5.1 Edition / June 2011, http://ecma-international.org/ecma-262/5.1/#sec-12.14

[5] Web-selainohjelmointi / syksy 2012 http://www.cs.helsinki.fi/group/java/s12-weso/

[6] Closures / November 2013 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures

[7] JavaScript-Garden / 2011 http://bonsaiden.github.io/JavaScript-Garden/fi/#function.closures

[8] Imperative vs Functional / http://kriszyp.name/2010/01/16/imperative-functional-and-declarative-programming/

[9] Learning JavaScript Design Patterns / Addy Osmani 2012 http://addyosmani.com/resources/essentialjsdesignpatterns/book/

[10] JavaScript. The core. / Dmitry Soshikov September 2010 http://dmitrysoshnikov.com/ecmascript/javascript-the-core/

[11] Classical Inheritance in JavaScript / Douglas Crockford http://www.crockford.com/javascript/inheritance.html

[12] Object.create() / November 2013 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create

[13] javascript : function and object…? / 2011 http://stackoverflow.com/questions/5958417/javascript-function-and-object

[14] Understanding “Prototypes” in JavaScript / Yehuda Katz August 2011 http://yehudakatz.com/2011/08/12/understanding-prototypes-in-javascript/

[15] GRASP (object-oriented design) / http://en.wikipedia.org/wiki/GRASP_(object-oriented_design)

[16] JavaScript Inheritance by Example / Klaus Komeda http://www.klauskomenda.com/code/javascript-inheritance-by-example/


Linkit keskeneräisiin materiaaleihin, joiden pohjalta tämä dokumentti on luotu.

Tyyppiturvallisuuden tavoittelua (2. viikko)
Hyviä ohjelmointikäytäntöjä: algoritmit, funktiot, sulkeumat, poikkeukset (3. viikko)
Hyviä ohjelmointikäytäntöjä: oliot ja periytyminen (4. viikko)