Primitiiv ja objektitüübid JavaScriptis
JavaScriptis saab numbreid, teksti jms esitada nii primitiivväärtusena kui ka objektina. Tehete puhul ei ole kummalgi variandil erilist vahet - numbrite liitmisel on tulemus ikka sama, hoolimata sellest kas kumbki operand on primitiivnumber (n. 10) või objekt (n. new Number(10)), vastus on ikka täpselt sama.
var nr_primitiiv = 10,
nr_objekt = new Number(10);
nr_primitiiv + nr_objekt; // 20
Samuti saab primitiivtüübina defineeritud väärtust kasutada sama tüübi objekti väärtusena, sellisel juhul teisendatakse väärtus operatsiooni läbiviimise ajaks objektiks.
var nr_primitiiv = 10;
nr_objekt = new Number(10);
nr_objekt.toFixed(2); // "10.00"
nr_primitiiv.toFixed(2); // "10.00"
Kusjuures viimast saab teha ka otse numbri endaga - arvestada tuleb ainult, et numbri ja punkti vahele (objekti meetodi eraldaja) tuleb jätta tühik, kuna vastasel juhul peab interpretaator punkti mitte meetodi, vaid komakoha eraldajaks.
10 .toFixed(2); // "10.00"
Samuti saab kasutada nurksulge, sellisel juhul pole vaja isegi tühikut kasutada
10["toFixed"](2); // "10.00"
Erinevused
Seega, mis kasu on üldse primitiivväärtuse objektina defineerimisel, kui toimingute läbiviimisel pole mingit vahet?
Tuleb välja, et üks erinevus siiski on. Selle erinevuse tuvastamiseks kontrollime kõigepealt väärtuste tüüpe.
typeof 10; // "number"
typeof new Number(10); // "object"
Nagu näha, on tavanumbri tüübiks number, aga objektina defineeritud väärtuse tüübiks oodatult object. Mis aga on nende kahe tüübi vahe? Vahe on väärtuse edastamises - kui primitiivväärtuse edastamisel edastatakse väärtus (by value), siis objekti edastamisel edastatakse objekt ise (by ref).
Kuna aga numbriobjekt on objekt, siis saab sellele lisada omadusi ja meetodeid nagu kõikidele teistele objektidele ning sellise numbriobjekti edastamisel (näiteks funktsiooni parameetrina) liiguvad need lisatud meetodid ja omadused numbriobjektiga kaasa.
var nr_objekt = new Number(10);
// defineeri numbriobjektile täiendav meetod
nr_objekt.teavita = function(){
alert(this);
}
// funktsioon ootab sisendiks objekti meetodiga "teavita"
function teavitus(nr){
nr.teavita();
}
// edasta numbriobjekt funktsioonile - by ref
teavitus(nr_objekt); // alert(10);
Ainukseks probleemiks on vaid see, et taolise objekti numbriväärtust ei saa enam muuta. Muutes objekti primitiivväärtust, lõhutakse objekt selle väärtuse ümber. Küll aga saab muuta väärtust, mis maailmale välja paistab, vt. järgmist punkti.
Veel rohkem lahedust
Numbriprimitiivi ja numbriobjekti liitmisel õnnestub tehe seetõttu, et objekti väärtuse puhul ei kasutata mitte objekti ennast, vaid selle meetodit valueOf, mille väärtuseks on samuti numbriprimitiiv.
10 + new Number(10).valueOf()
JavaScriptis aga on enamvähem kõik ülekirjutatav. Seega võib proovida üle kirjutada ka valueOf meetodit.
var a = new Number(10);
a.valueOf = function(){
return 5;
}
5*a; // 25 (5 * 5)
NB!
Kasutasin siin näitena vaid numbriprimitiive ja -objekte, aga sama laieneb ka kõikidele teistele väärtustele (string, boolean). Erineb vaid objekti väärtuse teisendamise meetod, stringi puhul on selleks valueOf asemel toString. Täpsemalt saab vaadata kuidas väärtusi konteksti alusel teisendatakse siit.
Objektid JavaScriptis
Panen siia kirja väikese ülevaate objektidest JavaScriptis.
Objektid JavaScriptis on lihtsad võtme-väärtuste paaride kogumid, millega on täiendavalt seotud veel ka kontekstimuutuja this. Juhul kui võtme väärtuseks on funktsioon, nimetatakse seda võtit objekti meetodiks, muudel juhtudel aga omaduseks. Objektideks võib pidada lisaks spetsiaalselt defineerituile JavaScriptis ka kõiki teisi kasutatavaid väärtusi, sealhulgas numbreid, teksti, loogilisi väärtusi, funktsioone jne. Kõikidel nendel väärtustel on olemas omad omadused ja meetodid. Näiteks numbriprimitiiv 10 ei ole iseeneest küll objekt, vaid 64 bitine number, aga proovides kasutada seda numbrit objekti kontekstis (n. 10 .toFixed(2)), teisendatakse primitiivtüüpi number taustal tehte läbiviimise ajaks automaatselt Number tüüpi objektiks.
Kui teistes keeltes defineeritakse reeglina kõigepealt looodavat objekti kirjeldav klass ja seejärel tuletatakse loodud klassi alusel objektid (kusjuures samast klassist tuletatud objektid on struktuurilt identsed), siis JavaScriptis klassi mõiste puudub ja objektide loomine on tunduvalt vabam. Alustada võib näiteks tühjast objektist (selleks saab kasutada objektiliteraale, loogelisi sulge var tyhi_objekt = {};), millele saab siis hiljem vajadusele vastavalt uusi omadusi/meetodeid lisada ning vanu kustutada.
Klasside asemel on JavaScriptis kasutusel nn. prototüüpobjektid. Ühe objekti prototüübiks võib olla suvaline teine objekt. Kui erinevad objektid on üksteise prototüüpideks, tekib sellega prototüübiahel - iga ahelas olev objekt saab kasutada kõiki ülemistes kihtides defineeritud prototüüpide omadusi ja meetodeid kui endagi omi. Juhul kui muuta prototüüpobjektis mõnd meetodit või omadust, muutub see automaatselt ka kõikides sellest prototüübist põlvnevates objektides. Samas ülespoole muutused ei kajastu - muudatused objektis selle prototüüpi ei mõjuta. Juhul kui objektis kirjutada mõni prototüübilt päritud omadus või meetod üle, katkeb selles kohas ahel ning objekt saab endale “isikliku” väärtuse (seda kas tegu on “isikliku” või päritud väärtusega, saab kotrollida meetodiga hasOwnProperty).
Ühetüübiliste objektide loomiseks saab klasside asemel kasutada konstruktorfunktsioone, kuid konstruktor pole sisuliselt midagi muud kui otsetee prototüüpobjekti kloonimisest ja initsialiseerimisfunktsioonist. Kuna JavaScripti esialgne disain jäi suhteliselt poolikuks, siis kuni praeguseni on konstruktorid olnud ainsad reaalsed (brauseriülesed) vahendid prototüübiahelate loomiseks. Alles ECMAScript5 tutvustas käsklust Object.create, mis võtab parameetriks loodava objektile omistatava prototüübi. Osad brauserid on mõnda aega toetanud ka objektide omadust __proto__, mis on otselingiks prototüüpobjekti juurde. Tegu on siiski mittestandardse ja vähe toetatud omadusega, seega reaalselt seda kasutusele võetud kunagi ei ole.
Objektide kirjeldamine
Objekte kirjeldatakse loogeliste sulgude vahel olevate komadega eraldatud väljadega, kus väljad koosnevad võtme nimest ja väärtusest. Võtme nime (võib olla nii jutumärkide vahel, kui ka ilma) järel on võtit ja selle väärtust eraldav koolon.
{
omadus: "väärtus1",
"meetod": function(){}
}
NB! Tähele tasub panna, et võti ”omadus” on esitatud ilma-, aga ”meetod” koos jutumärkidega. Jutumärkide kasutamine on oluline juhul kui võtme nimes on sümboleid, mida ei saa muutuja nimes kasutada, näiteks kui soovitakse kasutada miinusmärki või tühikut.
{
"eba-standardne nimetus":"väärtus"
}
Objekti omadustele ja meetoditele saab ligi kahel viisil - a) punktnotatsiooniga, kus objekt ja selle võtme nimetus on eraldatud punktiga või kui võtme nimetus on nurgelistes sulgudes objekti taga.
var objekt = {"nimi":"objekt"};
objekt.nimi === objekt["nimi"];
Kontekstimuutuja “this”
Iga objektiga on seotud ka kontekstimuutuja this mis võimaldab objekti meetodites pääseda ligi sama objekti teistele omadustele ja meetoditele. this sõltub ka skoobist. Kui tegu ei ole otseselt objekti meetodiga (näiteks anonüümne funktsioon meetodi sees), siis viitab this globaalsele objektile, milleks brauseri puhul on alati window.
var objekt = {
meetod: function(){
this == objekt; // true, meetodi skoop
function sisemine(){
this == window; //true, anonüümne skoop
}
}
}
Konteksti on võimalik ka funktsioonile ise ette anda, kasutades selleks funktsiooni meetodeid call ja apply. Sisuliselt on tegu samade meetoditega, ainult et kui call edastab argumendid nii nagu ta need ise saab, siis apply kasutab funktsioonile edastatavateks argumentideks sisendina saadud massiivi väärtusi.
function kontekst(){
alert(this==window);
}
kontekst(); // true, vaikimisi kontekstis
kontekst.call({}); // false, kontekstiks on anonüümne objekt
Alati ei ole aga vaja funktsiooni kohe käima panna, vaid millalgi hiljem (sündmuste jms tagasikutsed). Selliseks juhuks võimaldab ECMAScript5 kasutada meetodit bind, mis seob funktsiooni kontekstiga. Kuna paljud brauserid veel ECMAScript5 ei toeta, tuleb nendes see meetod ise defineerida.
var context = {text:"text to be displayed"};
function callback(){
alert(this.text);
}
window.onload = callback.bind(context);
NB! meetod bind mitte ei käivita funktsiooni ja ei tagasta selle tagastusväärtust nagu näiteks call ja apply, vaid tagastavad sama funktsiooniobjekti, kuid koos seatud kontekstiga! Väga lihtsustatult näeb sidumine välja nii:
Function.prototype.bind = function(context){
var func = this;
return function(){
func.call(context);
}
}
Funktsiooni meetodi näites saab func väärtuseks käesoleva funktsiooni enda. Seejärel tagastatakse väljakutsekohta anonüümne funktsioon. Kui nüüd see funktsioon käima panna (eelnevas näites window.onload sündmuse korral) käivitataksegi juba rida func.call(context) ehk et käivitatakse originaalne funktsioon, millele määratakse teine kontekst (sulundi kaudu tulnud context).