JavaScript edasijõudnutele

Autor: Andris Reinman

Avaldatud Creative Commons Autorile viitamine + Jagamine samadel tingimustel 3.0 Eesti (CC BY-SA 3.0) litsentsiga

www.tahvel.info/javascript:advanced

Tallinn 2011

Sissejuhatus

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.

Programmeerimine keeles JavaScript

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.

Andmetüübid

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

Primitiivsed tüübid

number - 64bit ujukoma number (IEEE 754).

string - Unicode tekst, pikkus pole otseselt piiratud. Pikemate tekstidega on opereerimine üsna mälumahukas, kuna reeglina tekstiväärtusi tehetes kunagi ei teisendata, vaid nende sisu kopeeritakse uude, vastloodud väärtusse.

boolean - loogiline väärtus, tõene (true) või väär (false)

null - eriväärtus tähistamaks olematut väärtust. Üheselt tuvastatav vaid samasusperaatoriga "==="

null === null

null !== false

null == false

undefined - defineeritud, kuid väärtustamata muutuja väärtus, pole sama mis 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)

Keerulisemad tüübid

massiiv - ühemõõtmeline massiiv, 0-algusega numbriline indeks, maksimaalseid elemente massiivis 4 294 967 295

objekt - võti-väärtus paaride kogum, kõige tähtsam andmetüüp

funktsioon - funktsiooniväärtus, mis sisaldab käivitatavaid programmilauseid.

viga - veaväärtus, mis indikeerib probleemi olemust

regulaaravaldis - regulaaravaldise muster tekstide töötlemiseks

aeg - kuupäeva- ja ajaväärtus, põhineb millisekunditel alates 01.01.1970

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.

Tehted väärtustega

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:

Loogiline JA (&&) - tagastatakse esimene mittetõene või viimane väärtus

Loogiline VÕI (||) - tagastatakse esimene tõene või viimane väärtus

Kõikidel muudel juhtudel (komad jne) tagastatakse alati lause viimane väärtus

Seega 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"

Numbrid

JavaScriptis on kõikide numbrite jaoks kasutusel üks 64 bitine ujukoma numbritüüp. Täisarvude puhul on maksimaalseks vahemikuks -2⁵³⁻¹ … 2⁵³⁻¹ 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

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 - massiiv funktsiooni parameetritega

arguments.length - funktsioonile edastatud parameetrite hulk

arguments.callee - funktsioon, mille alla arguments kuulub (NB! ES5 strict mode seadetes ei ole arguments.callee kasutamine enam lubatud)

arguments.callee.length - mitu parameetrit on deklaratsiooni alusel ette nähtud

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

Objektid

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

JSON

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.

Valideeruv JavaScript ja JSON: {"võti":"väärtus"}

Valideeruv JavaScript, kuid mitte JSON: {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

Prototüübid

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

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

Konstruktor - operaatoriga 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

Objekti kontekst

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

Konstruktorfunktsioonide kasutamine

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.

Sisseehitatud objektide täiendamine

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"
}

Tehted objektidega

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!

Getter/Setter

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

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.

Globaalne skoop

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

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!
    }
}

Isekäivituva funktsiooni skoop

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.

Lokaalne let skoop

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
}

Objekti skoop

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

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.

Privaatsed muutujad

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

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"

forEach

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

map

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]

reduce

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 1

1 + 2; - eelmine väärtus 1, hetke elemendi väärtus 2, summa 3

3 + 3; - eelmine väärtus 3, hetke elemendi väärtus 3, summa 6

filter

Masiivi 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]

Veahaldus ja erindid

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

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öö

Error objekt

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 koostamine

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 kohta

fileName - faili URL, milles viga tekkis

lineNumber - rida programmis, kus viga tekkis

stack - täpsem info vea tekkimise põhjuste kohta (Firefox, Webkit)

try/catch/finally

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.

onError sündmus

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

Protsess

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:

Esiteks väljastatakse nr 1

Teiseks seatakse taimer

Kolmandaks väljastatakse 3

Nüüd on koodiblokk läbi ja järg antakse brauserile, mis saab läbi viia omi vajalikke tegevusi

Brauser käivitab vahepeal kätte jõudnud taimeri

Väljastatakse nr 2

Node.JS puhul on sama mõistlikum teha funktsiooniga process.nextTick

console.log(1);
process.nextTick(function(){
    console.log(2);
});
console.log(3);

DOM

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

Elementide otsimine dokumendist

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

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.

classList

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 lisamiseks

element.classList.contains(klassi_nimi) kontrollib klassi olemasolu

element.classList.remove(klassi_nimi) kustutab klassi

element.classList.toggle(klassi_nimi) lisab/eemaldab klassi

Elementide atribuudid ja omadused

DOM 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.

Data atribuudid

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.

CSS

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

Sündmused

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 ülesehitus

Sündmuse ilmnemine toimub reeglina kolmes faasis

Püüdmisfaas (capture), rakendub alt üles sündmusega seotud elementide pihta

Rakendumisfaas (target), kävitub sihitud elemendi pihta

Mullitamine (bubbling), rakendub ülevalt alla sündmusega seotud elementide pihta

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ündmuse seadmine

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.

Sündmuse toimumise katkestamine

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

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

Ajax/Comet long polling

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.

Klient teeb Ajax päringu ja jääb ootele

… klient on ootel, päring kestab (readyState < 4)

Tekib mingisugune edastamistvajav sõnum

Server seab Ajax päringu vastuseks sõnumi ja katkestab ühenduse. (readyState == 4)

Klient saab sõnumi kätte ja taasavab koheselt uue päringu

Kuna tegu on väga lihtsa mehhanismiga, töötab selline lähenemine kõikides XMLHttpRequest toega brauserites.

Andmete striimimine Ajax päringuga

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.

Cross-domain XMLHttpRequest

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.

Server-Sent Events

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

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 skriptid (XSS) ja turvalisus

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.

XSS näited

eval

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.

Protokoll 'javascript:'

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.

Skripti kaasamine

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.

Cross-site request forgery

Ü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>

CSRF rünnaku vältimine

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.

Akendevahelised sõnumid

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

Workers

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.

Storage - Andmete salvestamine brauseris

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

Andmete hoidmine küpsistes

localStorage baas

userData 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

Flash storage - andmete salvestamine (kuni 100 kB) Flash plugina abil

Google Gears Database API (kuni 2 GB)

Silverlight

Java

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.

Andmete hoidmine küpsistes

Ü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.

localStorage baas

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

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.

SQLite andmebaas brauseris

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

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.

Andmebaasi avamine

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 lisamine

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ärimine

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

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 lisavorminguta

console.info - teksti kõrval sinine hüüumärgi ikoon

console.error - punane tekst punase ikooniga

console.warn - kollase hüüumärgiga

Lisaks 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.

Lugemist

https://developer.mozilla.org/en/javascript

http://tahvel.info/javascript

http://dmitrysoshnikov.com/ecmascript/es5-chapter-3-1-lexical-environments-common-theory/

JavaScript The Definitive Guide, Flanagan, D.

JavaScript: The Good Parts, Crockford, D.

High Performance JavaScript, Zakas, N. C.

JavaScript Patterns, Stefanov, S.