Ülevaade programmeerimiskeele JavaScripti võimalustest.
Autor: | Andris Reinman |
Litsents: | See teos on litsentseeritud Creative Commonsi Autorile viitamine + Jagamine samadel tingimustel 3.0 Eesti litsentsiga. |
EPub formaadis: | javascript.epub |
Kindle formaadis: | javascript.mobi |
Avaldatud: | Tallinn 2011 |
JavaScript on skriptimiskeel, mida kasutatakse peamiselt peremeeskonteineris nagu näiteks veebilehitseja. Algselt loodigi JavaScript Netscape nimelise brauseri juurde, kus seda sai kasutada muu hulgas veebivormide valideerimiseks, jättes sellega ära aeganõudva pöördumise veebiserveri poole (sel ajal kasutati internetti sissehelistamise kaudu) saamaks teada, et mingi väli oli valesti täidetud ning kõik tuleb uuesti teha. Hiljem standardiseeriti keel nimega ECMAScript (ECMA-262, ISO 16262) ning JavaScript muutus standardiseeritud keele dialektiks. Hetkel aktuaalne ECMAScripti versioon on järjekorranumbriga 5 (ECMA-262-5, edaspidi ES5), kuid suur osa brauseritest toetab veel vaid versiooni 3 (ECMA-262-3, edaspidi ES3). Versiooninumber 4 jäi vahele seoses erimeelsustega keeleuuenduste suhtes.
JavaScripti potentsiaal leviku poolest on meeletu - praktiliselt kõikides maailma arvutites, mis on ühendatud internetti, sisaldub veebibrauseri näol JavaScripti interpretaator. Tänaseks päevaks on JavaScript levinud brauserist ka paljudele teistele platvormidele, pakkudes sellega parimat võimalust kasutada sama programmikoodi võimalikult palju. Näiteks saaks algselt vaid brauseri jaoks kirjutatud e-posti aadressi valideerimisskripti kasutada veel ka serveris (Node.JS), mobiiltelefoni rakendustes (Appcelerator, PhoneGap), tabelarvutusprogrammis (Google Docs), televiisoris (digiboksid) või mõnes muus JavaScripti toega keskkonnas.
JavaScript keelena on suhteliselt omapärane. Puuduvad näiteks standardsed vahendid sisendiks ja väljundiks ning tegu on ka praktiliselt ainsa laiemat tuntust kogunud prototüübipõhise objektorienteeritud keelega. JavaScripti süntaks sarnaneb C ja teiste sarnaste keelte (Java, PHP jne) süntaksile. Nagu paljud teised skriptimiskeeled, on JavaScript dünaamiliselt ja nõrgalt tüübitud. Ka programmikoodi vormindamine on suhteliselt lõtv - kuigi lausete lõpetamisel on kasutusel semikoolonid, ei ole nende kasutamine kohustuslik - lause võib lõpetada ka reavahetus. Üldine soovitus oleks siiski semikooloneid nende valikulisusest hoolimata kasutada. Lisaks ei ole vaja erinevalt PHP'st või PERL'ist JavaScripti puhul vaja kasutada muutujate nimede ees dollari vms. sümboleid.
Antud konspektis on kirjeldatud mitmeid ES5 (ECMAScript 262 edition 5) ja HTML5 võimalusi. HTML5 on JavaScriptiga seotud uute brauseri API'de läbi (geolokatsioon, offline andmete salvestamine jne), kuid ei ole otseselt sõltuv ES versioonist. Webkit põhistes brauserites on ES5 võimalused vaikimisi kasutatavad, samuti ka IE8 (implementeeritud osaliselt) ja IE9 puhul. Mozilla brauserites tuleb ES5 kasutamiseks skripti laadides märkida aktiivseks JavaScripti versiooniks 1.8.5. Erinevalt teistest brauseritest on Mozillas implementeeritud ka suur hulk ES4 funktsionaalsust, kuna aga ES4 ei jõudnud kunagi lõpliku standardini (see jäeti pooleli, et keskenduda ES5 standardile), ei ole teised brauserid ES4 funktsionaalsust implementeerinud. HTML5 poolt pakutud võimalused on samuti implementeeritud eri brauserites eri ulatusega.
Tüüpe ei ole JavaScriptis väga palju, primitiivsetest on numbreid vaid üks ("number"
), tekstitüüpe üks ("string"
), lisaks loogilised väärtused (quot;boolean"
) ja mõned eritüübid (null ja "undefined"
). Täiendavateks ja keerukamateks tüüpideks on massiiv, objekt ("object"
), funktsioon ("function"
), viga, regulaaravaldis ja aeg (viga, aeg jne identifitseeritakse samuti kui objektiväärtusi).
true
) või väär (false
)"==="
null === null
null !== false
null == false
null
NB! Väärtus null
identifitseeritakse tegelikult objektina (typeof null === "object"
), kuid reaalsuses see seda pole (puuduvad meetodid ja omadused ning võimalus neid lisada)
Kõiki andmetüüpe saab omistada suvalisele muutujale, edastada parameetrina jne. See tähendab, et ka näiteks funktsioonid (mida JavaScript käsitlebki kõrgemat-järku objektidena) on samal viisil edastatavad väärtused - omistamisel ei saa muutuja väärtuseks mitte funktsiooni tagastusväärtust, vaid muutuja hakkab ise funktsiooni sisu kandma ning muutub käivitatavaks.
// anonüümse funktsiooni omistmine muutujale var a = function(b){ return b*2; }; a(4); // 4*2 = 8
Väärtuse tüüpi saab kontrollida operaatoriga typeof
, mis tagastab väärtuse tüübi tekstikujul. Väärtuseks võib olla "string"
, "number"
, "boolean"
, "function"
, "object"
või "undefined"
. Kõikidel muudel väärtustel (sh. null
ja massiivid), on tüübi väärtuseks samuti "object"
- mis tekitab aegajalt tihti üsna suurt segadust. Ning lisaks veel, ka kõigi primitiivväärtuste ("string"
, "number"
, "boolean"
) tüübi väärtuseks võib olla "object"
- seda siis juhul kui need väärtused on defineeritud konstruktorfunktsiooniga kujul new String()
. Seega väärtuse tüübi kontroll võib osutuda aegajalt üsna keeruliseks
nr_value = 123; alert(typeof nr_value); // "number" str_value = "abc"; alert(typeof str_value); // "string" str_value = new String("abc"); alert(typeof str_value); // "object" array_value = [1,2,3]; alert(typeof array_value); // "object"
Tuvastamaks objekti täpsemat tüüpi, saab kasutada operaatorit instanceof
, mis võtab operandideks objekti ning konstruktori, mille vastu objekti kontrollitakse.
var value = new Date(); value instanceof Date; // true value instanceof Number; // false
Massiivide mugavamaks tuvastamiseks tõi ES5 keelde uue funktsiooni Array.isArray()
.
Array.isArray([1,2,3]); // true Array.isArray(123); // false
Primitiivväärtuse ja selle objektilise esituse vahel on peamiseks erinevuseks väärtuse edastamise viis - kõik primitiivväärtused nende edastamisel (näiteks esitamisel funktsiooni parameetrina) kopeeritakse (by value), objektid aga viidatakse (by reference). Nii saab näiteks objektilisele stringile lisada meetodeid ja omadusi ning need kanduvad koos muutujaga edasi, primitiivväärtuse puhul see ei õnnestu.
var str_primitiiv = "tere tere!", str_objekt = new String("tere tere!"); str_primitiiv.omadus = "lisaomadus"; str_objekt.omadus = "lisaomadus"; var viide1 = str_primitiiv; // by value viide2 = str_objekt; // by reference alert(viide1.omadus); // undefined alert(viide2.omadus); // "lisaomadus"
Samas aga puudub sellisel käitumisel suresti praktiline väärtus, kuna objekti primitiivväärtus ei ole muteeritav.
Operaatorid on sarnased muude C laadse süntaksiga keeltega &&
, ||
, !=
jne. Lisaks on kasutusel dünaamiliselt tüübitud keeltele omane samasusoperaator (===
ja !==
), mis kontrollib lisaks väärtustele ka operandide tüüpi. Kui false == null
on tõene (väärtused on võrdsed), siis false === null
enam ei ole (väärtused ei ole samad).
Kõikidel tehetel on alati mingisugune tagastusväärtus (muu puudumisel on selleks undefined
) ning seda väärtust saab omistada muutujale, edastada funktsiooni parameetrina või kasutada juba järgmises tehtes. Loogiliste operaatorite puhul on tagastusväärtused järgnevad:
&&
) - tagastatakse esimene mittetõene või viimane väärtus||
) - tagastatakse esimene tõene või viimane väärtusSeega loogiliste operaatorite tagastusväärtus pole mitte loogiline (boolean
) vaid alati üks operandidest. Erandiks on vaid vastasusoperaator hüüumärk, mis tagastab loogilise vastandväärtuse.
alert( false || "tere" ); // "tere" alert( "tere" && 0 ); // 0 alert( !"tere" ); // false alert( !"" ); // true
Nii on mugav lähtestada näiteks funktsiooni parameetrite vaikimisi väärtusi - juhul kui parameeter on seadmata saab sellele OR
tehtega seada soovitatava väärtuse.
function(a){ a = a || "vaikimisi väärtus"; }
Juhul kui on siiski vaja tehte tulemusena kindlasti just loogilist väärtust (true, false), saab selleks kasutada vastasusoperaatorit kahekordselt, näiteks !!muutuja
.
alert( !!"tere" ); // true alert( !!"" ); // false
Kõiki muutujaid (nii primitiivtüübilisi kui objekte ja ka funktsioone) on võimalik üle kirjutada, v.a. üksikud erandeid, nagu reserveeritud sõnade nimekirjas olevaid nimetusi. Järgmine näide loob uue versiooni funktsioonist alert
, mis lisaks parameetrina saadud väärtusele väljastab ka selle väärtuse tüübi.
var old_alert = alert; // omista funktsiooniväärtus // kirjuta funktsioon uue väärtusga üle alert = function(value){ old_alert(value+", "+(typeof value)); } alert(123); // "123, number"
JavaScriptis on kõikide numbrite jaoks kasutusel üks 64 bitine ujukoma numbritüüp. Täisarvude puhul on maksimaalseks vahemikuks -253-1 … 253-1 ja komakohtadega 5E-324 … 1.79E+308, lisaks saab kasutada eriväärtusi +/- Infinity. Sellist tüüpi numbrid on suure vahemiku tõttu head võibolla teadusarvutusteks, kuid mitte kõige ideaalsemad „igapäevaprobleemide“ (näiteks tehted rahaga) lahendamiseks, kus vaja on numbri täpsust ning paari komakohta.
Kuigi iseenesest on väärtusvahemik lai, on reaalsuses sellest rusikareeglina kasutatavad vaid umbes 16 kohta ning selles sisaldub ka koma taga olev osa. Üle selle pikkuse muutub number juba ligikaudseks.
Seega 8.123456789101235 ja 562949953421312.1 on korrektsed ja ühesed, aga näiteks 562949953421312.123456789101235 enam ei ole, kuna on juba ligikaudne (võrdne teiste samas suurusjärgus numbritega). Suurte numbrite ligikaudsust saab kergelt kontrollida järgmise tehtega:
Math.pow(2, 53) === Math.pow(2, 53) + 1; // tõene!
Lisaks on komakohtadega numbritega tegelemine JavaScriptis alati üsna riskantne, kuna komakohtadega tehete tulemused on samuti tihti ligikaudsed. Üldlevinud näiteks on 0.1 ja 0.2 liitmine, mille tulemus pole sugugi 0.3. Tõene võrdlus on hoopis järgmine:
0.1 + 0.2 != 0.3 0.1 + 0.2 == 0.30000000000000004
Selline arvutus teeb aga rahaväärtustega opereerimise üsna keerukaks - kahe summa liitmisel on tulemuseks hoopis mingi muu, kui oodatud arv. Vahe ei ole suur, kuid võrdlustehetes (suurem/väiksem/võrdne) on see juba määrava tähtsusega - nii on lause 0.1 + 0.2 > 0.3
JavaScripti puhul tõene. Seega rahaga arveldades on parem kasutada täisnumbritega sente ja alles andmete kuvamisel jagada sendiväärtus euroväärtuse saamiseks sajaga läbi.
0.1*100 + 0.2*100 == 0.3*100
Kuid ka siin on omad ohud - kõiki numbreid ei saa nii lihtsalt täisarvuks muuta:
19.90 * 100 != 1990 19.90 * 100 == 1989.9999999999998
Seega kõige parem viis on sendiväärtuste kasutamisel veel täiendava ümardamise kasutamine
Math.round(19.90 * 100) == 1990
Kui aga sellise sendiväärtuse puhul on vaja esitada uuesti euroväärtust, saab seda mugavalt teha numbriobjektide meetodiga toFixed
.
alert((1990 / 100).toFixed(2)); // "19.90"
Funktsioonid on sarnaselt teistele keeltele alamprogrammid. JavaScript käsitleb funktsioone kõrgemat-järku objektidena, mis tähendab, et funktsioonilause pole pelgalt lausekonstruktsioon alamprogrammi loomiseks, vaid sellega luuakse väärtus, mida saab käivitada, seada muutuja väärtuseks, edastada funktsiooni parameetrina jne.
Funktsioone saab deklareerida kahel viisil, etteantud nimega ning anonüümselt. Anonüümselt deklareeritud funktsiooni saab omistada kohe ka muutujale, mis praktikas tähendab sama kui etteantud nimega deklaratsioon. Anonüümse funktsiooni eripäraks on, et seda saab kasutada programmis PEALE deklareerimist, samas kui nimega deklareeritud funktsioon on kasutatav terves skoobis (nn. hoisting).
func1(); // OK func2(); // undefined, pole veel deklareeritud function func1(){} // nimeline deklareerimine func2 = function(){} // anonüümne omistamine func1(); //OK func2(); // OK
Funktsiooni sees on kasutatav erimuutuja arguments
, mis on massiivilaadne väärtus (puuduvad massiividele omased meetodid) ja mis sisaldab kõiki funktsiooni parameetritena edastatud väärtusi hoolimata funktsiooni deklaratsiooni järgi määratud parameetrite hulgast - deklareeritud parameetrite hulga järgimine pole kohustuslik, seadmata jäänud parameetri väärtus funktsioonis on undefined
.
arguments.callee
kasutamine enam lubatud)function a(){ // väljasta esimese parameetri väärtus alert(arguments[0]); } a(123); // 123
Kuna funktsioonid on väärtused, annab see võimalusi suvalisi funktsioone kergelt „laiendada“ - selleks tuleb vana funktsioon lugeda asendusmuutujasse, asendada funktsioon soovituga ning käivitada ka asendusmuutujas olev originaalfunktsioon.
function originaal(a){ alert(a); } var asendus = originaal; originaal = function(a){ a = a*2; asendus(a); } originaal(4); // 8
Antud metoodikat kasutati varem väga tihti DOM Level 0 sündmuste juures - kuna iga sündmuse kohta (näiteks onclick või onload) oli elemendil ainult üks vastav muutuja, tuli selle laiendamiseks varem defineeritud väärtus üle kirjutada.
var old_onload = window.onload; window.onload = function(){ alert("tere tere!"); old_onload(); }
JavaScriptis puuduvad teistele OOP keeltele omased klassid. Traditsiooniliselt on klassid metainfo kogumid, mis iseloomustavad loodavaid objekte ning klassist tuletatud objekt on selle metainfo täpne realisatsioon - objekti struktuur on fikseeritud ning muuta saab vaid omaduste väärtusi, kuid mitte nende olemasolu. JavaScriptis klassid puuduvad ning nende asemel on kasutusel dünaamilised objektid (omadusi saab jooksvalt lisada/eemaldada), mis on omavehel ühendatud prototüübilinkidega.
Objektid on esitatud kujul {võti: "väärtus", …}
, kus võtme-väärtuste paarid on eraldatud komadega ning kus võti võib olla nii jutumärkides, kui ilma. Jutumärgid on vajalikud vaid juhul, kui nimi sisaldab lubamatuid sümboleid - tegu pole seega isegi mitte stringiväärtuse vaid rohkem hoopis paomärgistusega.
Juhul kui objekti võtme väärtuseks on funktsioon, nimetatakse seda võtit objekti meetodiks, vastasel korral aga omaduseks. Omadustele ja meetoditele saab ligi kahel viisil - punktnotatsiooniga ja kandiliste sulgudega. Viimasel juhul tuleb edastada võtme nimetus stringina - seega saab kandiliste sulgude puhul võtmeks olla ka muutuja väärtus.
var objekt = {võti: "väärtus"}; alert( objekt . võti ); // punktnotatsioon var nimi = "võti"; alert( objekt[nimi] );
Ka primitiivväärtusi on võimalik kasutada objekti kontekstis. Sellisel juhul luuakse tehte toimumise hetkeks primitiivtüüpi väärtuse ümber automaatselt vastav objekt, mis peale tehte lõppu kustutatakse. Nii saab täpselt samamoodi kasutada meetodeid tavaliste stringide või numbritega (numbrite puhul tuleb punktnotatsiooni kasutades jälgida, et numbri ja punkti vahele jääks tühik, vastasel korral loetakse punkt komakoha eraldajaks).
"tere tere".length; // 9 1234 .toFixed(2); // 1234.00 1234["toFixed"](2); // 1234.00
JavaScripti objektnotatsioonist tuleneb ka andmevahtusformaat JSON. Meeles tuleb pidada, et kui JSON on JavaScripti objektina praktiliselt alati valideeruv (v.a. paari väga harva kasutatava unikoodi tühemikusümboli korral tekstiväärtuste juures - U+2028 ja U+2029), siis JavaScripti objekti kirjeldus ei pruugi sugugi nii tihti valideeruv JSON olla. Nimelt on JSON’i puhul, erinevalt JavaScripti objektide kirjeldusest kohustuslik panna objekti võtmed jutumärkidesse.
{"võti":"väärtus"}
{võti:"väärtus"}
JSON stringide töötlemiseks tõi ES5 tulek kaasa objekti nimega JSON
, mis sisaldab meetodeid parse
ning stringify
.
JSON.stringify(value); // -> json string JSON.parse(json_string) // -> value
JavaScriptis klassid kui sellised puuduvad, nende asemel on kasutusel prototüübilingid. Olemasolevast objektist saab „kloonida“ uue instantsi ning orignaalne objekt jääb loodu prototüübiks. Kloonitud objekt ei ole alguses mitte iseseisev, vaid peegeldab prototüübi omadusi - niikaua kuni kloon sama väärtust enda poolt üle ei kirjuta.
⇣ BAASOBJEKT Object.prototype: {...} ⇣ A: { tyyp:"pall", värv:"sinine", suurus:"väike" } ⇣ B: { tyyp:↲ , värv:"roheline", suurus:↲ , lapik: "jah" } ⇣ C: { tyyp:"kera", värv:↲ , suurus:↲ , lapik:↲ }
Objekti C prototüübiks on B ning selle prototüübiks omakorda A. Juhul kui objekti C juurest küsitakse omaduse suurus
väärtust (alert(C.suurus);
), otsitakse seda kõigepealt objekti C juurest. Kuna seda ei leita (objektis C vastav omadus puudub), liigutakse prototüübiahelat pidi edasi objekti B juurde ning kui ka sealt otsitavat omadust ei leita, siis objekti A juurde. A juurest leitakse lõpuks omaduse väärtus ("väike"
) üles ning see tagastatakse esialgsesse väljakutsekohta juba kui objekti C väärtus. Reaalselt siiski omadust suurus
objektis C ei eksisteeri, selle asemel on ainult link prototüübi juurde. Juhul kui prototüübilink ära vahetada mõne teise objekti vastu, kus suurus defineeritud pole, „kaob“ see väärtus ka objektist C.
Kõikide objektide prototüübiahel algab baasobjektist, milleks on Object.prototype
(Object()
on konstruktorfunktsioon ning konstruktorfunktsioonidega on alati seotud prototüüpobjektid nimega prototype
). Baasobjekt annab kaasa hulga kasulikke meetodeid, mida saab kasutada objektide haldamisel. Näiteks seda kas mingi võti kuulub konkreetse objekti juurde või tuleneb see prototüübiahelast, saab kontrollida baasobjektilt päritud meetodiga hasOwnProperty
.
var objekt = {võti: "väärtus"}; objekt.hasOwnProperty("võti"); // true objekt.hasOwnProperty("hasOwnProperty"); // false, tuleneb baasobjektist
Omaduse meetodi olemasolu objektis saab kontrollida operaatoriga in
, kuid see enam päritud ja „omadel“ võtmetel vahet ei tee.
"võti" in objekt // true "hasOwnProperty" in objekt // true
Olemasolevate võtmete leidmiseks on kolm viisi
for..in
- Kõikide objekti võtmete loendamine (loendatakse ka päritud võtmed, v.a. mitte-loendatavad (enumerable)) for(key in objekt){alert(key);}
Object.keys
(ES5) - Võtmetest massivi koostamine (ei kaasa päritud võtmeid ega mitte-loendatavaid võtmeid) var keys = Object.keys(objekt);
Object.getOwnPropertyNames
(ES5) - Võtmetest massiivi koostamine (ei kaasa pärituid, kuid kaasab mitte-loendatavad võtmed)Nagu näha võivad võtmed olla loendatavad (enumerable, tulevad for..in
lausega välja) või mitte-loendatavad (ei tule for..in
lausega välja). Võtme loendatavust saab kontrollida objekti meetodiga propertyIsEnumerable
. Vaikimisi on kõik skripti poolt loodud omadused loendatavad.
Tihti kasutatakse konstruktsiooni for..in
ka massiivi indeksite loendamiseks, kuid see on halb lähenemine - kui massiivi prototüüpi on muudetud, edastatakse lisaks indeksitele ka massiivi prototüübi loendatavad võtmed. Parem on kasutada Array#forEach
(ES5) meetodit. Lisaks ei ole for..in
poolt edastatud võtmete järjestus garanteeritud (0,1,2,3 asemel võib tulla 0,2,1,3), mis massiivide puhul on üsna oluline.
[1,2,3,4,5].forEach(function(value){ alert(value); // ükshaaval 1,2,3,4,5 });
Enne ES5 tulekut oli võimalik objektidele ise lisada vaid loendatavaid omadusi, kuid ES5 võimaldab lisada ka mitte-loendatavaid kasutades omaduse seadmiseks näiteks defineProperty
meetodit (sarnase funktsionaalsusega võimalusi on ES5 juures teisigi).
var objekt = {}; Object.defineProperty(objekt, "omadus",{ value:1, // väärtus enumerable: false // ei ole loendatav });
Prototüüpide seadmine on kuni ES5 tulekuni olnud suhteliselt keeruline - siiani sai objekte kloonida vaid konstruktorfunktsioonide abil, mis tegi prototüüpidest arusaamise üsna keerukaks. Osad brauserid (Mozilla, WebKit) on võtnud kasutusele eriväärtuse __proto__
, mis viitab otse objekti prototüübile ja mida on võimalik ka muuta. Tulevikus peaks antud väärtus tõenäoliselt hetkel toetavatest brauseritest tehnilistel põhjustel kaduma (raskendab koodi kompileerimist) ning alles jäävad vaid ES5’ga seotud vahendid.
__proto__
- saab vaadata/muuta otse objekti juures var a={c:1}, b={}; b.__proto__=a; alert(b.c); //1
Object.create
- uut objekti luues saab määrata selle prototüübi var a={c:1}, b=Object.create(a); alert(b.c); //1
new
omistatakse muutujale funktsiooni prototüüp function a(){}; a.prototype.c=1; var b=new a(); alert(b.c); //1
Seda kas mingi objekt on teise prototüübiks või mitte saab kontrollida baasobjektilt päritud meetodiga isPrototypeOf
b.__proto__ == a; a.isPrototypeOf(b); // true
NB! Prototüüpide puhul tasub kindlasti meeles pidada, et kui prototüübi omaduseks on objekt ning üks prototüübist põlvnev objekt seda muudab, kanduvad muutused üle ka kõikidesse teistesse samast prototüübist tulenevatele objektidele. Kui aga tegu on primitiivväärtusega, siis seda ei juhtu.
var proto = { nested: { value: 1 }, value: 1 }, a = Object.create(proto), b = Object.create(proto); a.nested.value = 2; a.value = 2; alert(b.nested.value); // 2, muudatused tulid kaasa alert(b.value); // 1, muudatused ei tulnud kaasa
Iga objektiga on seotud kontekstimuutuja this
, mis on kasutatav muutujana objekti meetodites ja mis viitab objektile endale. Nii saab kutsuda objekti sees välja sama objekti teisi meetodeid ja omadusi teadmata objekti nime. Eriti oluline on see konstruktorfunktsioonide või anonüümsete objektide kasutamisel, kus loodud objekti muutuja nimi ei pruugi olla teada või seda ei eksisteerigi.
var objekt = { omadus = 34, meetod = function(){ alert(this.omadus); } } objekt.meetod(); // 34
Oluline on, et kontekstiks on alati funktsiooni „omanik“. Meetodi puhul on selleks objekt, kuid funktsiooni, mis pole defineeritud meetodina (sh. ka meetodite sees deklareeritud funktsioonid!) puhul on vaikimisi kontekstiks hoopis globaalne objekt window
. Eriti võivad segadust tekitada anonüümsed tagasikutsefunktsioonid, kus on samuti kontekstiks window
(ES5 strict mode seades sellist piirangut enam pole).
var objekt = { meetod: function(){ this === objekt; [1,2,3].forEach(function(val){ // anonüümse funktsiooni kontekst this === window; }); } }
Funktsiooni käivitamiseks etteantud kontekstis tuleb kasutada funktsiooni meetodeid call
või apply
, mõlemad võtavad esimese parameetrina kontekstimuutuja, kuid call
võtab seejärel funktsioonile edastamiseks parameetrite nimekirja ja apply
võtab samaks tarbeks massiivi.
function a(b, c){ this === window; alert(b); alert(c); } var kontekst = {d:1}; a.call(kontekst, 1, 2); // this === kontekst, b == 1, c == 2 a.apply(kontekst, [1, 2]); // this === kontekst, b == 1, c == 2
Tagasikutsefunktsioonide puhul - kui edastada tuleb funktsioon ise mitte selle tagastusväärtus - pole mainitud meetoditest väga palju kasu. ES5 tõi selle jaoks täiendava meetodi bind
mis seob funktsiooni juurde konteksti selleks hetkeks kui funktsioon ükskord käima pannakse.
function callback(val){ this === objekt; } var objekt = { meetod: function(){ [1,2,3].forEach(callback.bind(this)); } }
Nii saab massiivi meetod forEach
käivitatavaks tagasikutsefunktsiooniks funktsiooni callback
, millega on seotud kontekstimuutuja objekt. Tähele tasub panna, et bind
ei käivita funktsiooni ennast, vaid ainult seob sellega kontekstimuutuja - juhul kui kontekst pole funktsioonis oluline, võib täpselt sama panna kirja ka nii (this
väärtus funktsioonis oleks siis window
):
[1,2,3].forEach(callback);
Aga kuidas siis saada teada, millisele objektile viitab this
vaikimisi kui tegu on objekti meetodiga? call
, apply
ja bind
abil saab seda ka ise muuta, kuid mis on selleks vaikimisi?
Rusikareegel on, et tuleb vaadata funktsiooni käivitamise hetkel (st. sellel kohal, kus funktsiooniväärtuse taga on sulud) käivitatava väärtuse vasakut poolt - misiganes sealt vastu ei vaataks, see ongi kontekst.
Math.round(); // meetodi *round* kontekst on *Math* window.Math.round(); // kontekst on *window.Math*
Juhul kui seal pole midagi, on kontekstiks window
.
alert(); // funktsiooni *alert* kontekst on *window*
Kui tegu on väärtusena edastatud funktsiooniga, siis on kõik sama - arvestada ei tule mitte selle seadmise hetke, vaid käivitamise hetke
function foo(callback){ callback(); // <-- siin toimub käivitamine } foo(console.log); // <-- siin seadmine
Näites ei ole käivitatava meetodi log
kontekstiks mitte console
vaid window
! Kuna konteksti seab käivitamise hetk ning lause callback()
juures, erinevalt lausest foo(console.log)
enam konteksti määratud pole - vasakul pool kävitatavat väärtust pole märgitud midagi.
NB! Kui tagasikutse funktsioonile on meetodiga bind
kord juba kontekst seatud, siis seda call
või apply
abile muuta enam ei saa!
function bar(){ alert(this.baz); } function foo(callback){ callback.call({baz:1}); // <-- ei muuda enam midagi } foo(bar.bind({baz:2}); // <-- bind seab konteksti
Võimaldamaks kiirelt ja mugavalt luua sarnase ülesehitusega, kuid üksteisest sõltumatuid objekte, on JavaScriptis kasutuses konstruktorfunktsioonid. Konstruktor koosneb funktsiooniga seotud objektist prototype
(konstruktorfunktsiooni omadus, mis saab kõigi selle konstruktoriga loodud objektide prototüübiks) ning initsialiseerimisosast (funktsioon ise). Initsialiseerimise juures viitab this
juba automaatselt loodavale objektile. Konstruktoriks on iga funktsioon, mis käivitatakse operaatoriga new
.
function Konstruktor(){} // initsialiseerimisosa Konstruktor.prototype = {} // loodava objekti prototüüp var objekt = new Konstruktor(); // konstruktoriga objekti loomine
ES5 puhul oleks ilma initsialiseerimiseta samaks järgmine konstruktsioon
var objekt = Object.create(Konstruktor.prototype);
Juhul kui konstruktorfunktsioon tagastab objekti, tagastatakse see tagastusväärtusena. Juhul kui ei tagastata midagi või tagastatakse mitte-objekt, saab tagastusväärtuseks loodud objekt.
function Foo(){ this.bar = 1; return {bar: 2}; // konstruktori korral tagasta hoopis see objekt, mitte *this* } function Baz(){ this.bar = 1; return 2; // konstruktori korral seda rida ei arvestata, tagastatakse *this* }
Omadus prototype
on igal funktsioonil juba algselt lähtestatud tühjaks objektiks, seega ei ole vaja seda eraldi defineerida vaid soovi korral võib sellesse kohe lisada uusi meetodeid/omadusi.
Foo.prototype.bar = "baz";
NB! Kehtib tavaline funktsiooni deklareerimise reegel - juhul kui funktsioon on deklareeritud nimeliselt, saab omadust prototype
kasutada terve skoobi ulatuses, kui aga omistatud anonüümselt, siis alles peale funktsiooni deklareerimist.
Foo.prototype.bar = 1; // OK Baz.prototype.bar = 1; // Error function Foo(){}; Baz = function(){}
Kui konstruktorfunktsioon käivitada ilma operaatorita new
, siis uut objekti ei looda ning this
funktsiooni sees viitab globaalsele objektile window
. Kuna süntaks on aga igati valideeruv, siis sellist viga võib olla raske avastada.
JavaScriptis on kõigil objektiväärtustel lisaks prototüüpobjektile Object.prototype
ka oma täiendav tüübipõhine prototüüp, millest kõik seda tüüpi väärtused pärinevad (ja mis omakorda pärineb Object.prototype
prototüübist). Tüübi prototüübis on kirjeldatud konkreetse tüübi põhiomadused. Massiivi puhul on selleks prototüübiks Array.prototype
, regulaaravaldiste puhul RegExp.prototype
, vigade korral Error.prototype
, funktsiooni puhul Function.prototype
jne. Kuna tegu on objektiga, saab neid prototüüpobjekte ka laiendada ning nagu eelnevalt teada, laienevad prototüüpide juures tehtud muudatused kõikidele neist põlvnevatele väärtustele.
Näiteks kui lisada stringi baasobjektile String.prototype
meetod trim
(ES5 puhul on trim
juba vaikimisi implementeeritud, kuid ES3 puhul mitte),
Array.prototype.trim = function(){ return this.replace(/^\s+|\s+$/g,""); }
või ES5 korral, nii et lisatud meetod ei oleks loendatav
Object.defineProperty(Array.prototype, "trim", { enumerable: false, value: function(){ return this.replace(/^\s+|\s+$/g,""); } });
siis saavad sama meetodi endale kõik kasutatavad stringid
" tere, tere ".trim(); // "tere tere"
Nagu näha on meetodis kasutatud kontekstimuutujat this
- see viitab objektile, mille suhtes meetodit rakendatakse.
Vaikimisi on lisatavad omadused ja meetodid loendatavad (enumerable: true
, ES3 puhul muudmoodi ei saagi), seega võib see tekitada probleeme for..in
lause kasutamisel - eriti kehtib see, kui muudetud on kõikide objektide baasobjekti Object.prototype
. Näiteks kui massiivide prototüüpobjektile Array.prototype on lisatud mõni meetod või omadus, tulevad need for..in
lausega välja.
Array.prototype.circle = function(){ var tmp = this.shift(); if(tmp)this.push(tmp); } var arr = ["a","b","c"]; for(var key in arr){ alert(key); // 0, 1, 2, "circle" }
Samuti nagu kõiki teisi väärtusi, saab ka objekte kasutada erinevates tehetes ja operatsioonides. Samas on siiski primitiivväärtustega võrreldes mõned erinevused, mida kindlasti arvestada tuleb.
Objekte edastatakse omistamisel alati viitena (by reference), samas kui tavaväärtusi kopeeritakse (by value). Seega kui muuta kopeeritud objekti, muutuvad ka kõik teised sama objekti ilmnemised.
var foo = {bar: 4}, baz = foo; // by reference! baz.bar = 5; alert(foo.bar); // 5
Objektide võrdlemisel on objektid võrdsed ainult juhul (hoolimata, kas kasutatakse operaatorit ==
või ===
), kui võrreldakse üht ja sama objekti. Sarnaste, kuid mitte samade objektide korral on tulemus alati väär.
var foo, bar, baz; foo = bar = {value: 5}; baz = {value:5}; foo == bar; // true foo == baz; // false
NB! Sama kehtib ka massiivide kohta!
Objektidel saab määrata ka eritüübilisi meetodeid, mis paistavad välja hoopis omadustena. See tähendab, et objektil on omadus, millele saab määrata uut väärtust või seda lugeda, kuid tegelikult väärtuse määramise hetkel käivitatakse hoopis meetod (setter), mis saab sisendiks määratud väärtuse ning ka lugemisel käivitatakse meetod (getter), mille tagastusväärtust kasutatakse omaduse väärtusena.
objekt.setter_omadus = 1; // käivitatakse meetod parameetriga 1 val = objekt.getter_omadus; // käivitatakse meetod tagastusväärtuse saamiseks
Getter/Setter omaduste seadmiseks on mitu võimalust. Kuigi osad brauserid toetavad ka __defineGetter__
, __defineSetter__
meetodeid, ei tasu neid kasutada kuna tulevikus need eemaldatakse (tegu ei ole standardsete meetoditega).
Objekti defineerimisel saab kasutada täiendavaid parameetreid get
ja set
, mis muudavadki meetodi vastavaks omaduseks.
var objekt = { _tegelik_nimi: "pole veel", set nimi: function(nimi){ this._tegelik_nimi = nimi; }, get nimi: function(){ return this._tegelik_nimi+"!"; } }
Nii muutuvad meetodid get nimi
ja set nimi
ühiseks virtuaalseks omaduseks nimi
- mida reaalselt siiski ei eksisteeri.
alert(objekt.nimi); // "pole veel" objekt.nimi = "tere tere"; alert(objekt.nimi); // "tere tere!"
Getter/Setter tüüpi omadusi saab seada olemasolevatele objektidele meetodi defineProperty
(ES5) abil, nimelt lisaks omadusele value
saab määrata ka meetodid get
ja set
.
Object.defineProperty(objekt, "nimi", { set: function(nimi){ this._tegelik_nimi = nimi; }, get: function(){ return this._tegelik_nimi+"!"; } });
Skoop on programmiosa kehtivuspiirkond milles deklareeritud muutujad omavad väärtust ainult selles piirkonnas - skoopi väliselt muutujate väärtuseid lugeda ega muuta pole võimalik. Kui skoobi tegevused on lõppenud, eemaldatakse arvuti mälust kõik skoobiga seotud muutujad.
Erinevalt teistest C laadse süntaksiga keeltest kehtib skoop JavaScriptis ainult funktsioonipõhiselt - suvaline {…}
blokk skoopi ei tekita. Mozilla brauserites on lausega let
(ES4) täiendavalt võimalik luua ka lokaalseid skoope.
Globaalsesse skoopi kuuluvad kõik muutujad ja funktsioonid, mis on deklareeritud väljaspool funktsioone (või on üldse deklareerimata) ning nendele muutujate ja funktsioonidele pääseb ligi üle terve skripti. Näiteks kui üks javascript fail loob brauseris globaalse muutuja nimega foo
, siis see muutuja on ligipääsetav (loetav, muudetav) ka kõikides teistes samal HTML lehel olevates javascript failides.
Kui samal lehel on kasutusel mitmeid erinevaid kolmandate osapoolte teeke (jQuery, Prototype, TinyMCE jne) võib tekkida muutujate nimekollisioonide oht. Üks skriptiosa kasutab globaalset muutujat enda huvides, kuid teine osa skriptist kirjutab selle vahepeal üle, tekitades ilmselgelt oma käitumisega probleeme. Taolist kollisiooniohtu, kus skriptid kasutavad globaalse skoobi muutujaid, nimetatakse globaalse skoobi risustamiseks (global namespace polluting). Probleemi vältimiseks tuleb hoiduda globaalse skoobi kasutamisest ning kõik muutujad alati korrektselt deklareerida operaatoriga var
.
<script type="text/javascript"> globaalne_muutuja1 = 1; var globaalne_muutuja2 = 2; </script>
NB! Operaatori var
juures tuleb arvestada, et kui lähtestamisel kasutatakse konstruktsiooni var foo=bar="baz"
, siis tegelikkuses lähtestatakse vaid muutuja foo
! Nimelt võib sama lahti kirjutada kahe lausega bar="baz";
ja var foo=bar;
Kõik globaalse skoobi muutujad ja funktsioonid, olenemata nende deklareerimise viisist, on esitatud globaalse objekti window
omaduste/meetoditena ning seega ka vastavalt ligipääsetavad - punktnotatsiooni või kandiliste sulgude abil.
var foo = "bar"; // globaalse skoobi muutuja alert(foo); // "bar" alert(window.foo); // "bar" alert(window["foo"]); // "bar"
Funktsiooni skoop kehtib funktsiooni deklaratsiooni algusest kuni selle lõpuni. Skoopi kuuluvad kõik muutujad, mis on deklareeritud operaatoriga var
ning samuti ka kõik skoobis deklareeritud funktsioonid (v.a. anonüümsed funktsioonid, mis on omistatud mitte-lokaalsele muutujale).
function foo(){ // funktsiooni foo skoop var lokaalne_muutuja = 1; function lokaalne_funktsioon(){ var bar = 1; // ei kuulu enam *foo* skoopi! } }
Kohaliku skoobi loomiseks kasutatakse tihtipeale isekäivituvaid anonüümseid funktsioone. Sulgudes anonüümse funktsiooni deklaratsiooni järele pannakse täiendavad sulud, mis käivitavad selle funktsiooni koheselt peale deklareerimist. Nii kasutatakse ära suluoperaatorit, mis tagastavad nende vahel defineeritud väärtuse (anonüümne funktsioon), edastades selle funktsiooni käivitavatele sulgudele. Suluoperaator käitub vastavalt kontekstile - juhul kui sulud asuvad vahetult funktsiooniväärtuse järel, käivitavad nad funktsiooni; tehetes aga tagastavad sulgude vahel oleva tehte tagastusväärtuse.
(function(){ // isekäivituva funktsiooni skoop var lokaalne_muutuja = 1; })()
Sellist skoobi loomise meetodit kasutatakse rohkelt jQuery teegi juures.
Mozilla põhistes brauserites, mis toetavad ES4 versiooni funktsionaalsust, saab lisaks isekäivituvatele funktsioonidele kasutada lokaalse skoobi loomiseks veel lauset let
.
let(foo=23, bar=24){ // lokaalne let skoop var baz = 25; // foo, bar ja baz on lokaalsed muutujad }
Et vältida globaalse skoobi riknemist, kasutatakse tihti üht kindla nimega objekti omamoodi nimeruumina - kõik kasutatavad muutujad defineeritakse nimeruumiobjekti omadustena ning funktsioonid meetoditena. Nii saab luua globaalselt ligipääsetavaid muutujaid, mis samas ei lähe konflikti teiste skriptis esinevate muutujatega, kuna on kapseldatud ühise konteineri sisse.
Tegu ei ole otseselt skoobiga, vaid tehnikaga skoobi imiteerimiseks.
var NIMERUUM = {}; // globaalne unikaalse nimega muutuja NIMERUUM.globaalne_muutuja1 = 1; NIMERUUM.globaalne_funktsioon = function(){};
Sulundid (closures) on situatsioonid, kus ühe funktsiooni skoobis defineeritud funktsioonid omavad ligipääsu välisele skoobile, kuid väline skoop sisemisele ligipääsu ei oma.
function foo(){ // funktsiooni foo skoop var baz = 1; function bar(){ // funktsiooni bar skoop + funktsiooni foo skoop var qux = 2; alert(baz); // 1, sulund kehtib } alert(qux); // undefined }
Sulund tekib ainult sellisel juhul, kui sisemise funktsiooni skoop on deklareeritud välimise sees. Mujal deklareeritud funktsioone käivitades sulundit ei teki.
function foo(){ var baz = 1; bar(); // väljundiks undefined, sulund puudub } function bar(){ alert(baz); }
Sulunditega on mugav edastada erinevaid väärtusi (mis võivad vahepeal ka muutuda) sündmuste tagastusfunktsioonidele.
var foo = "bar"; link_element.addEventListener("click", function(evt){ alert(foo); // "baz" }, false); foo = "baz";
Nii on võimalik siduda erinevaid sündmuseid kindlate väärtustega ning neid väärtusi saab vajadusel ka muuta, ilma et oleks vaja tagasikutsefunktsioone uuendada.
Kuigi JavaScript privaatseid muutujaid ei võimalda, saab neid siiski sulunditega imiteerida.
function väline(){ var privaatne_muutuja = 1; return function(){ alert(privaatne_muutuja); } } var sulund = väline(); sulund(); // 1
Nii luuakse välise funktsiooni skoobis sisene sulundfunktsioon, mis tagastatakse funktsiooni tagastusväärtusena. Kui väline funktsioon käivitada, muutub muutuja sulund käivitatavaks funktsiooniks, millel on olemas ligipääs privaatsele muutujale.
Sulundite abil on võimalik luua ka keerulisemaid konstruktsioone, näiteks muutujate puhverdamist edaspidisteks tegevusteks. Järgmine näitefunktsioon võtab ükshaaval ja suvalise arvu kordi parameetrina numbreid ning tagastab nende numbrite summa
function add(a){ var retval = function(b){ a += b; return retval; } retval.valueOf = function(){return a} retval.toString = function(){return String(a)} return retval; } summa = add(1)(2)(3); // 6
Funktsioon puhverdab viimase väärtuse sulundimuutujas a
, millele liidab iga järgneva instantsiga laekuvad väärtused b
. Kui aga kasutada funktsiooni numbri (valueOf
) või stringi (toString
) kontekstis, on selle väärtuseks hoopis muutuja a
väärtus.
Massiivid on JavaScriptis ühemõõtmelised, 0-algusega numbriliste indeksitega ning massiivi elemendid ei pea olema ühte kindlat tüüpi (näiteks ainult numbrid), vaid nendeks võivad olla suvalised väärtused. Massiivid on dünaamiliselt laienevad ning seega ei ole vaja massiivi loomisel selle poolt hõlmatavat ruumi ära märkida - kui lisada massiivile element juurde, tehakse massiiv automaatselt suuremaks (v.a. juhul kui massiiv juba sisaldab maksimaalset elementide hulka, milleks on 4 294 967 295).
Erinevalt teistest keeltest ei ole tegu siiski mitte päris omaette admestruktuuriga, vaid JavaScripti objekti tüüpi väärtusega, millel on täiendavalt olemas omadus length ning Array.prototype objektilt päritud meetodid. Kuna numbrilisi indekseid ei saa punktnotatsiooniga kasutada, tuleb massiivi elementide poole pöörduda alati nurksulgude abil. Samas aga kaasnevad kõik objektide poolt pakutavad võimalused - näiteks saab ka massiividele lisada täiendavaid meetodeid ja omadusi.
var massiiv = ["a","b","c","d"]; massiiv.omadus = "foo"; massiiv.meetod = function(){};
Kõikidele massiividele täiendavate omaduste ja meetodite lisamiseks võib muuta Array.prototype
objekti. Seda muutmist tasub siiski teha ES5 meetoditega, mis võimaldavad uued võtmed muuta mitte-loendatavateks (vältimaks olukorda, kus for..in
lause abil massiivi indekseid loendades ilmuvad indeksite vahele ka omaduste/meetodite nimetused).
Object.defineProperty(Array.prototype, "foo", { value: function(){ alert("Massiivis on " + this.length + " elementi"); }, enumerable: false }); [1,2,3].foo(); // "Massiivis on 3 elementi"
Massiivi meetod forEach
(ES5) loendab kõik massiivi elemendid ning käivitab igaühega neist tagasikutsefunktsiooni. Nii on mugav rakendada mingit tegevust kõigi massiivi elementidega. Tagasikutsefunktsioon saab kolm parameetrit - hetke element, hetke indeks ning massiiv ise.
[1,2,3].forEach(function(value, i, arr){ alert("Element #"+i+": "+value); });
Massiivi meetod map
(ES5) võimaldab muuta ükshaaval kõiki massiivi väärtuseid ning tagastada muudetud väärtused uue massiivina. Kasutatav tagasikutsefunktsioon saab parameetriteks hetkel itereeritava elemendi ning selle indeksi.
Näiteks kui meil on massiiv numbritega ning soovime luua selle massiivi alusel uue, kuid milles oleks need numbrid koefitsendiga 2 läbi korrutatud, saab seda teha järgnevalt.
var vana_massiiv = [1,2,3,4,5], uus_massiiv = vana_massiiv.map(function(element, index){ return element * 2; }); // [2,4,6,8,10]
Meetod reduce
(ES5) võimaldab taandada massiivi üheks lõplikuks väärtuseks. Näiteks liita kõikide elementide väärtused kokku vms. Meetod töötab vasakult-paremale, st. et kõigepealt võetakse esimene väärtus, siis teine jne. Vastandiks on reduceRight
, mis teeb sama, aga vastupidises järjekorras. Tagasikutsefunktsioon saab parameetriteks viimase väärtuse, hetke iteratsiooni, selle indeksi ja kogu massiivi.
var massiiv = [1,2,3], summa = massiiv.reduce(function(prev, element, index, array){ return prev + element; }); // 6
Lahtikirjutatanuna oleksid tagasikutsefunktsiooni tehted järgmised
0 + 1;
- eelmine väärtus 0, hetke elemendi väärtus 1, summa 11 + 2;
- eelmine väärtus 1, hetke elemendi väärtus 2, summa 33 + 3;
- eelmine väärtus 3, hetke elemendi väärtus 3, summa 6Masiivi meetod filter
(ES5) itereerib üle lähtemassiivi kõigi elementide ning koostab nendest elementidest, mille puhul tagastusfunktsiooni väärtus on tõene, uue massiivi. Nii saab kokku massiivist korjata või välja jätta soovitud elemente. Tagastusväärtuse parameetriteks on itereeritav element ning selle indeks.
Näitena koostame massiivi, mis koosneb lähtemassiivis asuvatest paaritutest numbritest.
var vana_massiiv = [1,2,3,4,5,6,7,8], uus_massiiv = vana_massiiv.filter(function(element, index){ return element % 2; // paarisnumbri puhul on jääk 0 }); // [1,3,5,7]
Erind (exception) on märguanne, mis teatab erijuhu või vea tekimisest ning üldjuhul tähendab see mingisugust tõrkeolukorda. Käsuga throw saab tekitada uue erindi ning lausega catch saab selle kinni püüda. Kui erind ei ole kontrollitud (tekib väljaspool try blokki), peatab see terve programmi edasise töö.
throw tekitab veaobjekti ning lõpetab sellega programmi töö täitmise. Juhul kui throw asub try blokis, lõpetatakse bloki töö ja edastatakse veaobjekt catch blokile (juhul kui on seatud) ilma programmi enda täitmist segamata.
throw new Error("Ilmnes viga!"); // lõpetab programmi töö
Veatüüpe on erinevaid - Error
, EvalError
, RangeError
, ReferenceError
, SyntaxError
, TypeError
. Sisuliselt on tegemist sarnaste objektidega, välja arvatud objekti omadus name
, mis on võrdne vea tüübi nimega.
Näiteks SyntaxError
veaobjekti name
omaduse väärtus on "SyntaxError"
ning RangeError
objekti name
omaduse väärtus vastavalt "RangeError"
. See eristumine on vajalik catch blokis vea põhjuse määramiseks.
abc; //tekib *ReferenceError*, kuna "abc" on deklareerimata muutuja
Veaobjekti saab koostada nagu tavalist konstrukoriga objekti
new Error([teade [,failiNimi [,reaNumber]]);
ning reeglina käib see käskluse throw
juurde või omistakse muutujale. Juhul kui tavalise Error
objekti asemel on soov kasutada midagi muud, tuleb see nimi vastava tüübiga asendada, näiteks: new ReferenceError(…)
Veaobjekt koosneb järgmistest elementidest:
name
- tähistab vea tüüpi stringi kujul ("Error"
, "ReferenceError"
jne)message
- vabas vormis tekst selgituse vea põhjuse kohtafileName
- faili URL, milles viga tekkislineNumber
- rida programmis, kus viga tekkisstack
- täpsem info vea tekkimise põhjuste kohta (Firefox, Webkit)try
blokk võimaldab „proovida“ soovitud käsklusi. Kui blokis peaks ilmnema mingi viga, ei lõpetata programmi tööd, vaid info vea kohta antakse edasi blokile catch
. Lõpuks (hoolimata vea tekkimisest/mittetekkimisest) antakse järg üle blokile finally
.
catch
ja finally
blokid pole struktuuris kohustuslikud, kuid vähemalt üks neist peab try
blokile järgnema.
try{ // proovime mingeid käsklusi }catch(error){ // käivitub kui try blokis oli viga }finally{ // käivitub igal juhul }
catch
blokile edastatav parameeter error
on Error
objekt, mis sisaldab endas tekstikujulist selgitust vea ilmnemise ning vea tüübi kohta.
final
blokki kasutatakse üliharva, reeglina piisab lihtsamast try{…}catch(error){…}
konstruktsioonist.
Brauseris käivitatava JavaScripti puhul on võimalik kasutada onError
sündmuse seadmist. Sellisel juhul suunatakse kõik erindid sellele sündmuse juhtijale.
window.onerror = function(error, url, code){ // error - veaobjekt // url - fail, milles viga ilmnes // code - rea number return true; }
Juhul kui sündmust haldav funktsioon tagastab tõese väärtuse (true
), jätkab programm tööd, vastupidisel juhul töö lõpetatakse.
WebKit põhistes brauserites töötab onError
sündmus vaid kõige uuemates versioonides. Node.JS puhul saab püüdmata erindid kätte uncaughtException
sündmusega objektil process:
process.on('uncaughtException',function(error){ console.log("Ilmnes viga!"); });
JavaScript on ühelõimelise struktuuriga - juhul kui mingi programmikood parasjagu töötab, siis samal ajal midagi muud teha ei saa. Üldiselt ei ole see probleemiks, kuna üksikud koodiblokid ei ole väga pikad ja rakenduvad väga kiirelt, jättes seejärel programmi sündmuste tekkimise ootele.
Kui aga juhtub, et mõni programmiblokk on väga pikk või teeb midagi väga intensiivselt ning brauser kipub selle tagajärjel hanguma, tasub kaaluda selle bloki juppideks lõhkumist. Seda saab teha taimerite abil - seades taimeri pikkuseks 0 ms või mõne muu väga väikese arvu, rakendatakse taimeri blokk enamvähem kohe, aga samas tekib programmil aega tegeleda muude vahepeal tekkinud küsimustega.
Näiteks järgmise koodibloki puhul
console.log(1); window.setTimeout(function(){ console.log(2); },0); console.log(3);
on väljund järjekorras 1, 3, 2. Nimelt koodiblokki täidetakse järgnevalt:
Node.JS puhul on sama mõistlikum teha funktsiooniga process.nextTick
console.log(1); process.nextTick(function(){ console.log(2); }); console.log(3);
Dokumendiobjekti mudel ehk DOM on brauseri poolt pakutav liides veebilehtedega manipuleerimiseks. DOM’i baasobjektiks on document
, millest hargnebki dokumendipuu. Lisaks on saadaval veel näiteks objekt navigator
brauseri aknaga manipuleerimiseks, kuid see ei ole enam DOM’i osa - DOM hõlmab vaid konkreetselt veebielehega seotud struktuuri, kuid mitte seda ümbritsevat kasutajaliidest.
Lehel nähtavale osale saab kergelt ligi omadusega document.body
. Dokumendi päisele sama moodi siiski ligi ei saa - päisele ligipääsuks saab kasutada järgmist päringut
// vanemad brauserid var head = document.getElementsByTagName("head")[0]; // uuemad brauserid var head = document.querySelector("head");
Prototype, jQuery jms. teegid saavutasid oma populaarsuse suuresti DOM elementide lihtsa leidmise võime tõttu. Näiteks kui oli vaja leida lehelt üles kõiki linke, millel oli kindel CSS klass, siis DOM meetoditega getElementsByTagName
jms oli see üsna keeruline. jQuery aga pakkus lihtsat selektori põhist otsinguviisi - $("a.classname")
, $("a[class~=name]")
jne.
Praeguseks aga on olukord muutunud, kõik uuemad brauserid toetavad samal otstarbel sisseehitatud kiireid meetodeid querySelector
ja querySelectorAll
- esimene tagastab esimese tingimusele vastava elemendi, teine aga kõikidest vastavatest elementidest moodustatud massiivilaadse objekti. Sisendiks on mõlemal CSS selektor.
Juhul kui kasutusel on querySelectorAll
(või ka getElementsByTagName
vms.), siis tulemus tundub olevat massiiv, kuid tegelikult see seda pole - tegu on massiivilaadse objektiga, täpsemalt NodeList
tüüpi DOM spetsiifilise väärtusega. NodeList
on „elav“ list, mis tähendab, et kui vahepeal lehel midagi teha, siis see kajastub ka juba varasemalt tehtud päringu tulemustes.
var paragrahvid = document.querySelectorAll("p"); // X elementi document.body.appendChild(document.createElement("p")); paragrahvid.length == X+1; // üks element rohkem
Selektorid on otsingustringid, mis kirjeldavad otsitavate elementide omadusi.
querySelector("p"); // leia paragrahvi element
Elemendile saab lisada ka täiendavaid tingimusi. Trellidega saab määrata ID väärtuse ja punktiga klassiväärtuse - klassiväärtus kehtib klassilisti elemendi kohta. See tähendab, et kui elemendil on klassi nimeks tühikutega eraldatud nimed, siis iga nime järgi saab eraldi otsida.
querySelector("p#esimene"); // leia paragrahv, mille ID on esimene querySelector("p.esimene"); // CSS klass sisaldab nimetust esimene
Samuti saab ka suvaliste atribuutide järgi otsida, kui kasutada kandilisi sulge
querySelector("p[title=tekst]"); // leia paragrahv, millel title=text
Selektoridega saab koostada üsna keerulisi päringuid, kuid reeglina on siiski nii, et mida lihtsam päring, seda kiiremini see läbi viiakse. Erandina on ülimalt lihtsustatud päringud (näiteks ID järgi) - nende jaoks on spetsiaalsed meetodid juba olemas, mis on kindlasti ka kiiremad kui querySelector
, sest pole vaja teha enam selektoriga tekstitöötlust.
CSS klassidega opereerimine on muutunud üsna oluliseks - nii seetõttu, et nendega on mugav lisada korraga elementidele erinevat tüüpi omadusi (üks klass lisab värvi, teine kuju jne) kui ka seeõttu, et CSS klasse saab kasutada ka mugavaks markerisüsteemiks - klassile jäetakse näiteks kujundus määramata, küll aga oskab programm selle klassi olemasolu/mitteolemasolu järgi elemendis omi järeldusi teha.
Elemendile saab korraga mitu klassi nime omistada kui nimed eraldada tühikutega.
<div class="klass1 klass2 klass3"/>
Iga klass nimekirjas olevatest on iseseisev - juhul kui CSS blokis on ära kirjeldatud klass "klass1"
kujundus, siis see rakendub ka sellele elemendile.
Programmiliselt seni aga nii lihtne pole olnud - elemendi klassiväärtus on saadaval ühe pika stringina omadusest className
.
element.className; // "klass1 klass2 klass3 klass4"
Ilmselgelt on sellise stringiga opereerimine üsna tülikas - tuvastamaks kas mingi klass on olemas või mitte, tuleb teha stringitöötlust. Veel keerulisem on lisada uusi klasse listi - eelnevalt tuleb kontrollida kas see juba olemas pole - või neid kustutada.
Nende probleemide lahendamiseks on uuemates brauserites DOM elementidele lisatud omadus nimega classList
, mis võimaldab opereerida klassinimedega ükshaaval, mitte enam ühise pika stringina.
classList
näol on tegu järjekordselt massiivilaadse objektiga, kus elementideks on üksikud klasside nimed. Lisaks omab objekt veel erinevaid meetodeid nende nimedega opereerimiseks.
element.classList.add(klassi_nimi)
uue klassi lisamisekselement.classList.contains(klassi_nimi)
kontrollib klassi olemasoluelement.classList.remove(klassi_nimi)
kustutab klassielement.classList.toggle(klassi_nimi)
lisab/eemaldab klassiDOM elementidel eksisteerivad korraga atribuudid ja omadused. Atribuut tuleneb HTML koodis määratud atribuutidest. Näiteks elemendil <input value="ABC" />
on üks atribuut - value
, mille väärtuseks on "ABC"
. Atribuute saab lugeda ja kirjutada meetoditega getAttribute
ja setAttribute
ning atribuut on alati objekt.
Omadus aga on elemendi kui objekti omadus, mida saab lisada/eemaldata tavalise omaduse viisil.
var element = document.querySelector("p"); p.omadus = 1;
Kuigi osad brauserid käituvad standardi vastaselt (nimelt Internet Exploreri erinevad versioonid), siis tegelikult peaksid omadused ja atribuudid olema üksteisest sõltumatud - st. et elemendil võivad korraga eksisteerida nii sama nimega omadus kui ka atribuut.
Kuigi enda määratud nimega atribuute on saanud HTML’is kasutada kogu aeg, sh. ka XML nimeruumiga määratud atribuute (nimeruum:atribuudi-nimi
), ei ole need meeldinud HTML süntaksi validaatoritele. HTML5 tutvustas kohandatud atribuutide jaoks data-*
atribuute. Sellised atribuudid on kättesaadavad element.dataset
objekti kaudu.
Juhul kui elemendi HTML kuju on järgmine
<p data-nimi="paragrahv"/>
siis atribuut data-nimi
on ligipääsetav element.dataset.nimi
omaduse kaudu - tasub tähele panna, et kui HTML vormis eelneb atribuudi nimele prefiks "data-"
, siis dataset
omadusena seda enam atribuudi nime ees pole.
Elementide CSS stiilid on ligipääsetavad omaduse style
abil. Kuigi osaliselt tundub tegu olevat otseteega style
atribuudi juurde, siis tegelikult on see hoopis CSS2Propertie
objekti tüüpi vahekiht. See aga tähendab, et on täiesti korrektne toimetada omadusega element.style.display
, aga mitte omadusega element.style["margin-width"]
. Kõik style
omadused on camel-case
formaadis, seega korrektne viis oleks kasutada hoopis element.style.marginWidth
omadust.
Meeles tasub ka pidada, et numbriliste väärtuste lisamisel, tuleb alati lisada ka mõõtühik. Näiteks 5 pikselise pikkuse seadmisel element.style.width = 5;
ei tööta, selle asemel tuleb kasutada element.style.width = "5px";
Elemendi stiili saab muuta ka CSS tekstiga omaduse element.style.cssText
abil, kuid arvestada tuleb, et see kustutab kõik varemseatud stiilid, kuna asendatakse korraga kogu elemendi style
atribuut.
Lisaks üksikute elementide stiilidele on võimalik jooksvalt lisada ja üle kirjutada ka CSS klasse. Lehel kasutatud CSS blokid leiab omadusest document.styleSheets
, kuid mõistlikum on olemasolevate muutmise asemel lisada uus.
var styleSheet = document.createElement("style"); styleSheet.setAttribute("type", "text/css"); document.querySelector("head").appendChild(styleSheet); var cssRules = styleSheet.sheet.rules;
Objekt cssRules
on seejärel kasutatav CSS reeglite lisamiseks, mida saab teha meetodiga insertRule
.
cssRules.insertRule("body{font-family: Arial;}", cssRules.length);
insertRule
esimeseks parameetriks on lisatav CSS koodiblokk ja teiseks indeks, millisele positsioonile see lisada. Mõistlik on see lisada kõige lõppu (ehk positisoonile cssRules.length
).
JavaScript on sündmuspõhine keel. See tähendab, et peale skripti käivitumist, saab see seada üles erinevaid sündmuste kuulareid, mis jäävad peale skripti lõppu kuuldele. Kui nüüd juhtub mõni kuulatav sündmus (näiteks kasutaja klikib veebilehel kindlale elemendile) mille kohta sündmuse kuular on seatud, käivitataksegi seatud tagasikutsefunktsioon.
Globaalseteks sündmusteks on taimeri väljakutsed (pole küll tehniliselt sündmused, kuid käituvad sarnaselt „päris“ sündmustele).
window.setTimeout(function(){ alert("taimer läbi!"); }, 100); // käivita 100 ms pärast peale ülesseadmist
Brauserites DOM puuga seotud sündmused.
var esimene_a = document.querySelector("a"); esimene_a.addEventListener("click", function(event){ alert("klikkisid lehe esimesel lingil"); },false);
Node.js serverirakendustes erinevad EventEmitter tüüpi väärtused.
var s = stream; s.on("data", function(error, chunk){ console.log("tüki sisu: "+chunk); });
Sündmuse ilmnemine toimub reeglina kolmes faasis
Näiteks kui kasutaja klikib mingil elemendil hiirega, siis reeglina jääb klikkimise asukohta rohkem kui üks element. Näiteks paragrahvis <P>
asuva <SPAN>
elemendi sees oleva lingi puhul koosneks elementide puu järgmiselt:
<body><p><span><a> link </a></span></p></body>
- BODY ↳ P ↳ SPAN ↳ A
Seega klikkides lingil <A>
, tabame ka elemente <SPAN>
, <P>
ja <BODY>
. Püüdmisfaasi korral käivitatakse sündmus ükshaaval alates elemendist <BODY>
kuni <SPAN>
. Rakendumisfaasis käivitatakse sündmus elemendile <A>
ning mullitamisfaasis tagurpidi alates elemendist <SPAN>
kuni <BODY>
.
Seda, millise faasiga parasjagu tegemis on, saab kontrollida sündmuse omadusega event.eventPhase
.
Sündmust saab sündmuseid toetavale elemendile seada meetodiga addEventListener
. Meetod saab parameetriks kõigepealt sündmuse tüübi (n: "click"
või "mouseover"
), seejärel tagasikutsefunktsiooni, mis käivitatakse sündmuse rakendumisel ja viimasena loogilise väärtuse tähistamaks kas sündmust kuulatakse püüdmisfaasis (true) või mitte (false). Reeglina püüdmisfaasi siiski ei kasutata ning see tuleneb tõigast, et IE8 ja vanemad brauserid seda lihtsalt ei toeta (tõeline DOM Level 2 tugi, milles on ka sündmused ära kirjeldatud, tekkis Internet Explorer brauserisse alles versiooniga 9). Samas püüdmisfaasi kasutamine/mittekasutamine tuleb siiski true või false väärtusega ära fikseerida - vastasel korral tekib programmiviga.
Element.addEventListener("click", function(event){ … }, false);
Sündmust saab eemaldada meetodiga removeEventListener
, kuid oluline on, et meetod võtab parameetriteks täpselt samad väärtused, kui sündmuse seadmisel - see tähendab, et sündmust saab eemaldada vaid juhul, kui sündmuse seadmiseks kasutatud funktsioon on mingil kujul veel ligipääsetav - kui seatud on anonüümne funktsioon, siis täpselt samasuguse funktsiooni eemaldamine ei mõjuta midagi.
Igas sündmuse faasis saab sündmust haldav funktsioon selle sündmuse edasikandumist järgmistele elementidele peatada. Näiteks lingil <A>
klikkimisega käivituv sündmus võib peatada sündmuse edasise rakendumise lingi <A>
all olevatele elementidele.
Seda kas sündmust saab katkestada või mitte, näitab omadus event.cancelable
. Sündmuse peatamiseks on kaks meetodit, mida saab kasutada sõltuvalt katkestuse soovitavast iseloomust.
event.preventDefault()
hoiab ära vaikimisi tegevuse, mis toimuks juhul kui sündmuse korral peaks sihtelemendiga midagi täiendavat toimuma. Näiteks kui soovitakse lingil klikkides hoida ära brauseri liikumist lingi aadressile.
var a = element_link; a.addEventListener("click", funcition(event){ event.preventDefault(); // klikkides ei navigeerita lehelt ära }, false);
event.stopPropagation()
hoiab ära sündmuse edasise kulgemise sündmuse ahelas. Kui klikkida lingil <A>
ning sellel tekkiv sündmus kutsub esile stopPropagation
meetodi, ei kutsuta seda sündmust enam välja järgmistel mõjutatud elementidel (n: <BODY>
).
var ul = element_ul, li = element_li; // asub UL sees li.addEventListener("click", funcition(event){ event.stopPropagation(); // kliki sündmus ei mõju ülemelemendile UL }, false);
Ajax päringuks nimetatakse asünkroonset XMLHttpRequest päringut (XMLHttpRequest toetab ka blokeeruvaid päringuid, kuid neid ei saa enam Ajax päringuteks lugeda - need ei ole asünkroonsed), kus skript saab kasutajast sõltumata taustal serveri poole pöörduda, serverile andmeid saata ja neid pärida.
Kõige lihtsam Ajax päring näeb välja järgmine (tehakse GET
päring aadressile /url
ning väljastatakse selle sisu ekraanile)
var req = new XMLHttpRequest(); req.open('GET', '/url', true); req.onreadystatechange = function (aEvt) { if (req.readyState == 4) { alert(req.responseText); } }; req.send(null);
Reeglina kõik raamistikud (jQuery, Prototype jne) üritavad seda kuidagi lihtsustada, lisaks on vaja ka mõned erisused lisada (eelkõige vanemate Internet Exploreri versioonide jaoks, kus võivad muidu tekkida mälulekked).
Pikk pöördumine on meetod andmete kiireks ja vahetuks esitamiseks, kus samas uusi andmeid ei teki väga kiirelt. Näiteks brauseripõhine jutukas vms.
Brauser teeb serverile tavalise Ajax päringu, kuid server ei vasta ennem kui tal on mida vastata (näiteks teine kasutaja saatis jutuka-sõnumi) või saab etteantud ajalimiit täis (mõistlik aeg võiks olla näiteks 50 sekundit). Seejärel avatakse sama ühendus koheselt uuesti.
readyState < 4
)readyState == 4
)Kuna tegu on väga lihtsa mehhanismiga, töötab selline lähenemine kõikides XMLHttpRequest toega brauserites.
Kuigi tavaline XMLHttpRequest päring on mõeldud serverist kindlate andmete pärimiseks, saab sama kanalit kasutada ka striimimiseks. Nimelt kui tavalise päringu korral kontrollitakse readyState
väärtust, ootuses et see on 4
, mis tähendaks päringu lõppemist, siis samuti on võimalik kontrollida väärtust 3
, mis tähendab et laekus järjekordne osa vastusest. Päringu vastus responseText
on selleks hetkeks aga juba loetav (v.a. IE6 ja IE7 puhul), seega on loetav ka äsja laekunud jupp vastusest. Kui nüüd server ühendust ei sulge, vaid saadab kogu aeg andmeid juurde, saabki antud meetodit kasutada striimimiseks.
Arvestada tuleb vaid, et olemasolev tekst ei sisalda mitte ainult viimati laekunud osa, vaid kõiki andmeid, seega tuleks pidada jooksvalt arvet, palju senini andmeid laekunud on ja see osa siis koguandmetest maha arvata.
var lastLength = 0; xhr.onreadystatechange = function(){ if(xhr.readyState == 3){ alert("Uued andmed:\n" xhr.responseText.substr(lastLength)); lastlength = xhr.responseText.length; } }
Antud lähenemine ei tööta brauseris Opera, mis kutsub sama readyState
väärtusega onreadystatechange
sündmuse esile vaid ühekordselt. Lisaks on Internet Explorer 8 puhul vaja kasutada päringu sooritamiseks XDomainRequest objekti ning täiendavalt tuleb veel arvestada, et IE8 puhul esimese 2 kB andmete korral striimimist ei toimu - readyState==3
sündmus kutsutakse esile alles siis, kui andmeid on kogunenud rohkem kui 2 kB. Toetatud on ka vaid text/plain
tekstitüüp.
Kuigi Ajax päringutele kehtib same origin policy, on kõikides uuemates brauserites võimalik teha päringuid ka „võõrastesse“ serveritesse. Seda võimaldab XMLHttpRequest Level 2 omadus, mis kontrollib päringu tegemisel vastuspäise kirjet Access-Control-Allow-Origin
. Juhul kui see kirje on seatud ning ei välista päringut tegevat serverit (kirje väärtuseks on lubatud pöörduja nimi, tärni korral piirangud puuduvad) viiakse Ajax päring ellu.
Näiteks kui server määrab vastuse järgnevalt:
<?php Header("Content-type: text/plain; Charset=UTF-8"); Header("Access-Control-Allow-Origin: *"); echo "Hello world!"; ?>
siis saab selle skripti pihta teha Ajax päringu suvaliselt domeenilt (ligipääsupiirang puudub, kuna lubatud väärtuseks on tärn).
Internet Explorer 8 puhul tuleb XMLHttpRequest objekti asemel kasutada hoopis objekti XDomainRequest.
Serveri saadetud sündmused, tuntud ka nimega EventSource, on protokoll andmete edastamiseks serverilt brauserile striimina. Brauser teeb serverile Ajax laadse päringu, kuid ühe lõpliku vastuse asemel jätab server ühenduse püsti ning hakkab kliendile saatma striimi kujul teateid. Brauser oskab seda striimi vastu võtta ning kutsub iga saabuva sõnumi korral skriptis välja sündmuse onmessage
.
Firefox seda protokolli hetkel veel ei toeta, tugi on olemas brauserites Opera ning Chrome.
var source = new EventSource('updates.cgi'); source.onmessage = function (event) { alert(event.data); };
Serveripoolse otsa implementatsioon on väga lihtne - server saadab lihtsalt andmeid kujul data: andmerida
, kus andmeblokid on omavahel eraldatud tühja reaga ning iga andmerida algab märgendiga data:
data: kaherealine sõnum, esimene rida data: teine rida data: üherealine sõnum data: kolmas sõnum, samuti üherealine
Serveripoolse skripti korrektne mime-tüüp on text/event-stream
WebSocket on protokoll kahesuunaliseks suhtluseks brauseri ja serveri vahel. Kui Ajax päringu puhul tehakse serverile päring ning sellele saadakse ühene vastus või kui EventSource puhul tehakse serverile päring ning vastuseks avatakse sisenevate sündmuste striim, siis WebSocket on täielikult kahesuunaline - mõlemad pooled, nii brauser kui ka server võivad igal hetkel sama ühendust kasutades üksteisele sõnumeid saata. Brauser avab ühenduse serveriga, tehakse WebSocket protokolli „käepigistus“ ning õnnestumise korral kutsutakse brauseris välja sündmus "onopen"
. Edaspidi saab serveri saadetud sõnumeid breauseris vastu võtta sündmusega "onmessage"
ning neid saata meetodiga send
.
Firefox 4 toetab WebSocket protokolli, kuid vaikimisi on see turvalisuse kaalutlustel välja lülitatud. Väidetavalt lülitatakse see sisse tagasi, kui protokoll saab olulistest vigadest üle. Kuna brauseris on tugi siiski olemas, saab vajadusel selle toe käsitsi sisse lülitada - about:config
lehel tuleb otsida üles kirje network.websocket.override-security-block
ja seada selle väärtuseks true. Peale väärtuse muutmist on WebSocket protokoll koheselt kasutatav.
var ws = new WebSocket("ws://ws.example.com"); ws.onopen = function(event) { alert("Connection opened!"); }; ws.onmessage = function(event) { alert("Message from server:\n"+event.data); };
Serveripoolne ots on keerulisem, kuna ühenduse avamise kinnituseks tuleb esiteks läbi viia korrektne käepigistus. Seejärel saab hakata edastama ja vastu võtma UTF-8 teksti, mis on pakitud korrektsete märgendite vahele.
Protokolli täpset kirjeldust serveripoolse implementatsiooni loomiseks saab lugeda aadressilt http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-05 (NB! jälgi versiooni numbrit, uueneb tihti)
Cross-site skriptimine (XSS) on turvaaukude kasutamine, kus pahalane lisab kolmanda osapoole saidile oma skripti, mis käivitub juhul kui suvaline külastaja seda kolmanda osapoole saiti kasutab.
Kõige levinumaks viisiks on turvamata kommentaarikastidesse ja foorumitesse tekstide postitamine, mis sisaldab <script> silte. Juhul kui postitust vastuvõttev skript ei ole piisavalt tark, lisataksegi selline tekst veebilehele kommentaari või foorumipostitusena. Kui nüüd lehele satub mõni juhuslik kasutaja, <script> sildis olev koodiblokk käivitub ja viib oma tegevused ellu.
Unustada ei tohi aga lisaks otsestele <script> blokkidele ka igasuguseid atribuutidega seatavaid sündmuseid. Näiteks liikus Twitteris ringi „viirus“ mis kasutas ära võimalust lisada linkidele onmouseover
sündmus - kui kasutaja hiirega selle lingi peale sattus, viirus käivitus ja paljundas ennast kasutaja konto abil.
Kui funktsioon eval
käivitab skriptina teksti, mis kasutab ebausaldusväärset allikat (näiteks brauseri veebiaadressi riba), on programm kerge häkkerite saagiks langema. Näiteks järgmine skript avab teateakna hetkel eesoleva veebilehe aadressiga (teisendades aadressis erisümbolid arusaadavale kujule, 0%C3%B5=>õ
).
eval("alert('"+decodeURI(document.location.href)+"')");
Kui me nüüd viitame seda skripti sisaldava lehe juurde, aga ennem muudame veidi lingi sisu, näiteks selliseks:
<a href="http://example.com/skript.html'); alert('Ootamatu?" > http://example.com/skript.html </a>
siis veebilehte avades saab pahaaimamatu ohver ekraanile juba veidi teistsuguse teate. Konkreetsel juhul avaneb esmalt aken mis kuvab oodatult veebilehe aadressi. Peale teate sulgemist aga avab leht ootamatult juba teise teateakna, sisuga „Ootamatu?“, mida lehe tegijad plaaninud ei olnud.
Ainult teateakendega mängimine on tühiasi, kuid paraku on sama tehnikat kasutades võimalik saavutada palju enamat. Näiteks sessioonide varastamist vms. Kuna JavaScriptil on reeglina ligipääs lehel olevatele küpsistele, sh. sessiooni küpsisele, pole eriliseks probleemiks kõikide küpsiste andmed pahalastele saata. Saades kätte sessiooni võtme saab pahalane sessiooni üle võtta ja ise teise kasutajana autoriseeritult toiminguid teha. Õnneks võõralt pangaarvelt ülekandeid teha nii ei saa, aga e-maili kontole võib ligi saada küll.
Varem levisid ringi sarnast tehnikat kasutavad sotsiaalse võrgustiku Orkuti viirused. Kasutaja sai kuskilt hea „koodi,“ mis võimaldas saata sõnumeid korraga kõikidele oma sõpradele - tuli kopeerida saadud kood brauseri veebiaadressi reale ja seejärel leht avada. Mingit lehte ei avanenud, see kood lisas hoopis eesoleva lehe päisesse uue <script>
sildi, mille sisuks laadis brauser pahalase tegeliku skripti.
javascript:alert('Saadan sõnumit!'); document.getElementsByTagName('head')[0].innerHTML += '<script id="xss"></script>'; document.getElementById('xss').src='/paha.js';void(0);
Kui antud URL kopeerida aadressiribale ja üritada seda veebiaadressina ka avada, ilmub esiteks petlik teade „Saadan sõnumit!“, mis peaks kasutaja maha rahustama, tegelikult aga laetakse ja käivitatakse lehel üks täiendav skript ("/paha.js"
). Laetav skript saab õigused opereerida hetkel ees oleva veebilehega täpselt nii nagu vaja on, kuna skript „laetakse“ veebilehe enda poolt.
Üks säärane Orkutis levinud skript näiteks muutis ohvriks langenud kasutaja nime Pablo'ks ja vahetas samuti ära kasutaja profiili pildi. Õnneks ei suutnud skript muuta kasutajate paroole, nii et oma kontod said kasutajad tagasi, küll aga põhjustas see palju segadust.
Sama tehnikat on võimalik kasutada aga ka positiivsetel eesmärkidel, nii on tehtud erinevad abistavad bookmarklet’id - brauseris salvestatud järjehoidja on tegelikult javascript:
protokolliga skript, mis siis avamisel teeb erinevaid kasulikke asju. Näiteks loeb lehel hetkel valitud tekstiosa ja avab uue blogipostituse lehe, kus eelnevalt valitud tekst on automaatselt koos koos lingiga tsitaadiks seatud.
Osad teenused pakuvad enda komponentide lisamist kasutaja veebisaidile (mängud, küsimustikud, ilmateade, kaunterid jms.). Selle jaoks tuleb lisada soovitud kohta lehekülje HTML koodis skripti laadimise rida
<script src="http://example.com/komponent.html?kasutaja_id=ZZZ"></script>
Ning laetava skripti sisuks on lehele omapoolsete andmete lisamine näiteks käsu document.write
abil
document.write('<h1>Minu teenus</h1>');
Kui aga sellise skripti allikaks on ebausaldusväärne pakkuja, võib skript sisaldada ka pahalase koodi. Skript võib näiteks võtta kõik küpsiste andmed ja saata need pahalasele. Samas kui komponendi pakkuja server on aeglane, peatab see ka kasutaja lehekülje esitamise. Nimelt on JavaScripti laadimine eksklusiivne - niikaua kuni skripti sisu pole alla laetud, midagi muud ei tehta. Seega võib esineda situatsioon, kus jõutakse laadida osa lehest ja see brauseris ka esitada, aga edasi jääb lehele ainult tühi pind.
Üheks XSS erivormiks on Cross-site request forgery (CSRF). Tegu on eriti kavala rünnakuga, kuna kasutatakse ära vastuvõtva serveri usaldust kasutaja vastu. Nimelt kui kasutaja on kuhugi portaali sisse loginud ning brauseris on selle puhul seatud vastavad küpsised, tehakse kõik edaspidised päringud serverile koos nende küpsiste andmetega - mis tähendab, et serveril on igati põhjust neid päringuid usaldada. Samas aga võib selle päringu teha hoopis keegi teine, kui kasutaja ise.
Kuigi AJAX päringute tegemine on võõra domeeni pealt komplitseeritud, siis selliseid päringuid (nii GET
kui POST
), kus otsene tagasiside pole vajalik, saab erinevate meetoditega sooritada suvalise domeeni pihta väga edukalt. Kui kolmas osapool teeb päringu mõnele teenusele, eeldades et hetke kasutaja on teenuses sisse logitud ja päringuga lähevad kaasa kasutaja autoriseerimisandmed, siis ongi tegu CSRF päringuga. Näiteks avad foorumi lehe, sellel olev skript teeb päringu Twitteri lehele ning üritab postitada sinna sinu nime alt mingit teksti. Twitteri puhul see ei tööta, kuna sait on praeguseks korralikult rünnete vastu valmistunud, kuid enamuste teiste puhul saab nii teha küll.
<!-- IFRAME, mida kasutaja ei näe (asub "serva taga") --> <iframe id="sihtframe" style="position: absolute; top: -1000px"></iframe> <!-- Eeltäidetud andmetega vorm, mis suunab "panka" --> <form id="form" method="post" action="http://panganet/ylekanne" target="sihtframe"> <input type="hidden" name="saaja" value="Kuri Häkker" /> <input type="hidden" name="konto" value="123456" /> <input type="hidden" name="summa" value="1 000 €" /> </form> <!-- Saada eeltäidetud vorm "panka" teele --> <script> document.getElementById("vorm").submit(); </script>
Kõige lihtsam ja kindlam viis rünnaku vältimiseks on lisada igale vormile unikaalne tõendiväärtus, mis edastatakse peidetud väärtusena vormi saatmisel serverile. Juhul kui päring sisaldab korrektset tõendit, saab päringu vastu võtta, vastasel juhul mitte. CSRF meetodiga ei ole tõnedi teadasaamine ja kasutamine võimalik.
<form method="post" action="/postita"> <input type="hidden" name="token" value="unikaalne_tõendiväärtus" /> … </form>
Nagu näha on üldiselt CSRF rünnaku läbiviimine siiski ülimalt lihtne ja tõenäoliselt suurem osa veebiteenustest on selle vastu ka kaitsetud. Juhul kui kasutada moodsamaid raamistikke, siis need kipuvad vorme korrektselt käitlema, ise tehes või aegunud raamistike puhul võib probleem aga üsna tõsiseks osutuda.
Kuni ES5 saabumiseni oli praktiliselt võimatu kahel aknal (olgu need siis brauseri eri aknad või eri freimid) üksteisega mingilgi viisil suhelda, kui need asusid eri domeenidel. Praegu aga on võimalik kasutada sõnumiedastuse meetodeid, mis võimaldavad ühest aknast ja freimist teise saata ja vastu võtta tekstilisi andmeid.
Sõnumite kuulamiseks tuleb üles seada "onmessage"
sündmuse kuular.
window.addEventListener("message", function(event){ if(event.origin != "http://lubatud_saatja_domeen") return; alert(event.data); }, false);
Sõnumite saatmiseks aga saab kasutada window
meetodit postMessage
. Näites üritab IFRAME elemendis olev skript saata sõnumit aknale, mille sees ta asub.
parent.postMessage("sõnum", "http://lubatud_parent_domeen");
Kuigi JavaScript on ühelõimeline, toetavad uuemad brauserit ka täiendavate lõimede kasutamist taustal toimingute tegemiseks, ilma et põhiprotsessi sellega häiriks. Neid täiendavaid lõimesid nimetatakse Worker’iteks.
Workerid asuvad välistes failides ning neid saab ellu kutsuda järgmise konstruktsiooniga
var worker = new Worker("worker.js");
Suhtlus Workeri ja põhiprogrammi abil käib sarnaselt tavalise sõnumite vahetusele postMessage
ja "onmessage"
abil. Vahe on vaid selles, et Worker saab kasutada postMessage
meetodit ilma sihtakent määramata. Samuti jäävad ära lubatud domeeni parameetrid.
Worker on üsna piiratud - puudub ligipääs DOM’ile, põhiprogrammi skoobile jne. Küll aga saab teha Ajax päringuid.
Lisaks ajutisele lehekülje puhvrile, on võimalik andmeid brauseris salvestada ka püsivamalt. Andmete salvestamiseks kasutaja brauseris on peamiselt võimalik kasutada viit erinevat meetodit (kuid mitte ainult).
localStorage
baasuserData behavior
(Internet Explorer)SQLite
andmebaas brauseris (WebKit, Opera)IndexedDB
NoSQL andmebaas (Firefox 4, Chrome 11)Lisaks on veel kasutuses mõned kolmandatest osapooltest sõltuvad meetodid
Kolmandate osapoolte meetodeid võib muidugi arvestada, aga nende kasutamine ei ole alati soovituslik. Eriti arvestades, et näiteks Google Gears arendus on seoses HTML5 esilekerkimisega peatatud. Kui localStorage
kasutamiseks pole vaja muud, kui paar rida koodi, siis Flash storage jaoks peab alati kasutama ja sisse laadima välise SWF faili.
Üks vanemaid ja „lollikindlamaid“ viise andmete säilitamiseks kasutaja brauseris on laialt tuntud (ja kasutatud) küpsised.
Küpsistel on aga paar suurt probleemi, mis teevad nad sõltuvalt olukorrast vahel üsnagi kasutuks. Esiteks on küpsistel väga piiratud maht ja seda nii küpsise sisu kui küpsiste arvu suhtes. Arvestada saab ühe domeeni kohta 20 küpsisega, millest igaüks võib olla kuni 4096 sümbolit pikk. Reaalselt need numbrid erinevad brauserite kaupa, kuid suurusjärk peaks olema igal pool enamvähem sama. Arvestada tuleb ka, et küpsise sisu tuleks korrektselt kodeerida, see aga toob küpsise poolt pakutavat mahtu veelgi alla, kuna kõik erisümbolid võtavad kodeerituna rohkem ruumi.
Teisks suureks probleemiks (eriti just suure külastatavusega saitide korral) on fakt, et küpsise infot liigutatakse edasi-tagasi serveri ja brauseri vahel iga päringu korral (sh. pildid, css jne). Kui nüüd arvestada, et kasutuses ongi maksimaalne ruum 80kB (20*4096), siis serverile tekib sellest täiesti arvestatav koormus. Isegi kui päritakse kõige lihtsamat ja väiksemat faili, kaasneb selle päringuga ikkagi 80 kB liikumine brauseri ja serveri vahel.
Kolmas probleem (mis polegi tegelikult üldse probleem, aga millega tuleb teinekord arvestada), on see, et osad brauserid ei luba sättida küpsist teiselt domeenilt, kui parasjagu eesoleva lehe domeen. Näiteks kasutaja vaatab lehekülge www.domeen.ee ning see veebisait sisaldab endas pisikese pildi näol loendurprogrammi domeenilt www.loendur.ee. Kui www.loendur.ee üritab koos mainitud pisikese pildi laadimisega seada kasutaja brauserisse küpsist, siis suure tõenäosusega see ebaõnnestub.
Küpsiste kasutamisel on siiski üks väga suur pluss, mida ühelgi teisel meetodil pole - see töötab praktiliselt kõikide brauseritega, nii uute kui vanadega.
Küpsised asuvad JavaScriptis muutujas document.cookie
. Tegu on getter/setter tüüpi elemendiga - juhul kui sellele omistada väärtus, siis lisatakse see küpsiste nimekirja, kui aga lugeda, saab kätte ühe pika stringina kõik saadaolevad kõpsiste väärtused.
Koos HTML5 tulekuga tekkis küikidesse uuematesse brauseritesse localStorage
element. Tegu on window
objekti omadusega, millele saab omistada tekstilisi väärtuseid. Eriliseks teeb omaduse aga nende väärtuste säilimine lehe sulgemise ja avamise vahepeal.
if("localStorage" in window){ if(aeg = window.localStorage.aeg) alert("Viimati külastatud: "+aeg); window.localStorage.aeg = Date(); }
Näites kontrollitakse, kas localStorage
sisaldab omadust aeg
ning kui väärtus on olemas, siis väljastab selle ekraanile. Järgmiseks seatakse omaduse aeg
väärtuseks hetke aeg tekstikujul. Kui nüüd brauseris sama leht jälle lahti võtta, on omaduse aeg väärtus juba puhvris olemas ning kuvataksegi eelmise lehevaatamise ajainfo.
localStorage
suudab mahutada juba tunduvalt suuremat infomahtu, kui küpsised. Firefox eraldab näiteks 5 MB domeeni kohta ning IE8 isegi 10 MB. Tegu on ka kõige lihtsama salvestusmehhanismiga üldse, kuna ei erine tavalisest väärtuse omistamisest muutujale.
Firefox 2.0 ning 3.0 ei toeta kahjuks localStorage
baasi - selle asemel saab kasutada sarnast globalStorage
baasi. Erinevuseks on vaid fakt, et globalStorage
ei ole mitte ise baasiks, vaid kujutab endast baaside massiivi, kus massiivi võtmeteks on domeeninimed, mille kohta antud baas kehtib.
window.globalStorage[document.domain].aeg = Date();
Tänu oma lihtsusele on localStorage
viimasel ajal väga suurt poolehoidu saavutanud.
userData behavior
on Internet Exploreri spestiifiline laiendus, mis lubab suvalise DOM elemendi juurde salvestada infot ning hiljem seda taas laadida. Võimaldatud maht on üsna väike (128kB), kuid kuna lahendus töötab ka vanemates Internet Exploreri versioonides, on see lahendus siiani osati kasutusel.
WebKit põhised brauserid (Safari, Chrome jne) ja Opera sisaldavad viimastes versioonides SQLite andmebaasi, millele on ligipääsuks loodud ka JavaScript API.
Kuna tegu on hüljatud lahendusega - üldine suundumus on võtta tulevikus kasutusele IndexedDB - siis siin väga pikalt antud lahendusest juttu ei tee.
IndexedDB on NoSQL andmebaasimehhanism, mis sarnaselt localStorage
baasile võimaldab salvestada võtme/väärtuste paare, kuid andmeid baasist pärides ei pea erinevalt localStorage
meetodist võtme väärtuseid üheselt teadma ning korraga saab välja lugeda ka terve võtmevahemiku, mitte ainult ühe kindla väärtuse.
IndexedDB päringud käivad promises tüüpi asünkroonse metoodikaga. See tähendab, et iga päring tagastab päringu ootamise objekt, nn. promise’i, millele saab seada juba täpsemad sündmuste kuularid (onsuccess, onfailure).
Nagu teistegi NoSQL schemaless andmebaaside juures, ei ole ka IndexedDB baasis andmete hoidmiseks tabeleid. Selle asemel on salvestuskogumid, nn. store’d. Tegu ei ole tabeliga, kuna salvestatud andmed ei ole kindlalt lahtertadud väljadega - iga rida võib olla erineva struktuuriga.
Baasi kasutamiseks tuleb see kõigepealt avada, tegu on asünkroonse tegevusega ning enne baasi avanemist sellega majandada ei saa.
var db = window.indexedDB.open("dbname", "description"); db.onsuccess = function(event){ // baas on nüüd avatud }
Ridade lisamiseks tuleb kõigepealt valida õige store, seejärel saab asünkroonse meetodiga add kirjeid lisada.
… var store = event.result.objectStore("storename"); var promise = store.add(andmeobjekt); promise.onsuccess = function(event){ var id = event.result; // lisatud elemendi ID }
Ridade pärimisel saadakse tulemuseks kursorobjekt, mis võimaldab ükshaaval üle päringule vastavate kirjete itereerida.
request = event.result.objectStore("storename").openCursor(); request.onsuccess = function(event){ var cursor = event.result; if(cursor){ … cursor.continue(); } }
Firebug on asendamatu töövahend Firefox brauseriga JavaScripti programmide kirjutamisel. Firefox brauseris on tegu täiendava pluginaga, kuid teised brauserid on tänaseks Firebugi eeskujul sarnase töökeskkonna otse brauseri sisse ehitanud (WebKit, IE). Üheks kasulikumaks võimaluseks on console
objekti meetoditega info väljastamine Firebugi töölauale. Seda saab teha jooksvalt ja programmi tööd segamata - kui näiteks funktsioon alert
peatab kuni kasutaja reageerimiseni kogu programmi töö, siis console
väljastab infot eraldi aknasse ja programmi enda töösse ei puutu.
Erineva vormistusega teksti väljastamiseks on eraldi käsud
console.log
- väljastab teksti ilma igasuguse lisavormingutaconsole.info
- teksti kõrval sinine hüüumärgi ikoonconsole.error
- punane tekst punase ikoonigaconsole.warn
- kollase hüüumärgigaLisaks on veel console.dir
massiivi elementide/objekti võtmete ükshaaval kuvamiseks, console.group
, console.groupCollapsed
ja console.groupEnd
sõnumite grupeerimiseks jne.
console.group("teated"); console.log("tavaline teade"); console.info("sinise hüüumärgiga teade"); console.error("punase ristiga teade"); console.warn("kollase hüüumärgiga teade"); console.groupEnd();
Kuid veelgi kasulikumaks omaduseks võib teinekord osutuda profileerimine, mida Firebug võimaldab. Profileerimine mõõdab määratud aja jooksul programmi tööd ja kuvab seejärel info kui palju kordi ja kui kaua mingit tegevust tehti. Nii on kerge leida üles programmi osi, mis mingil põhjusel programmi tööd aeglasemaks teevad.
Profileerimist saab läbi viia Firebug aknas nupuga profile, kuid sama saab suurema täpsuse saavutamiseks teha ka programmiliselt käskudega console.profile
ja console.profileEnd
.
console.profile(); … // erinevad tegevused console.profileEnd();
Samuti saab mõõta tegevuste täpset kestvust funktsioonidega console.time
ja console.timeEnd
.